Skip to content

Commit 816ac86

Browse files
authored
Merge pull request #7 from stackrox/mc/quay-cred-test
Refactor docker credential fetching to fail fast
2 parents 20158e2 + 2baf9ae commit 816ac86

4 files changed

Lines changed: 121 additions & 38 deletions

File tree

pkg/deployer/deploy_via_operator.go

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -157,20 +157,18 @@ func (d *Deployer) prepareNamespace(ctx context.Context, namespace string) error
157157

158158
if env.CurrentClusterType != env.InfraOpenShift4 {
159159
if err := d.ensurePullSecretExists(ctx, namespace); err != nil {
160-
return fmt.Errorf("could not create pull secret: %w", err)
160+
return fmt.Errorf("ensuring image pull secret exists: %w", err)
161161
}
162162
}
163163

164164
return nil
165165
}
166166

167167
func (d *Deployer) ensurePullSecretExists(ctx context.Context, namespace string) error {
168-
pullSecretYAML, err := d.dockerAuth.CreatePullSecretYAML(namespace)
169-
if err != nil {
170-
return fmt.Errorf("could not create pull secret: %w", err)
171-
}
168+
// Assemble pull secret YAML from pre-verified credentials
169+
pullSecretYAML := d.dockerAuth.CreatePullSecretYAMLFromCredentials(d.dockerCreds, namespace)
172170

173-
_, err = d.runKubectl(ctx, KubectlOptions{
171+
_, err := d.runKubectl(ctx, KubectlOptions{
174172
Args: []string{"apply", "-f", "-"},
175173
Stdin: strings.NewReader(pullSecretYAML),
176174
})

pkg/deployer/deployer.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ type Deployer struct {
5454
useOLM bool
5555
verbose bool
5656
earlyReadiness bool
57+
dockerCreds *dockerauth.Credentials
5758
}
5859

5960
func New(log *logger.Logger, overrideFile string, overrideSetExpressions []string) (*Deployer, error) {
@@ -140,6 +141,11 @@ func (d *Deployer) Deploy(ctx context.Context, component, resources, exposure st
140141
d.portForwardEnabled = adjustedPortForward
141142
d.exposure = exposure
142143

144+
// Prepare and verify credentials early to fail fast
145+
if err := d.prepareCredentials(); err != nil {
146+
return fmt.Errorf("failed to prepare credentials: %w", err)
147+
}
148+
143149
d.logger.Infof("Initiating deployment of %s", formatComponentName(component))
144150

145151
switch component {
@@ -157,6 +163,24 @@ func (d *Deployer) Deploy(ctx context.Context, component, resources, exposure st
157163
}
158164
}
159165

166+
// prepareCredentials prepares and verifies Docker credentials early to fail fast.
167+
// The verified credentials are stored for later use.
168+
func (d *Deployer) prepareCredentials() error {
169+
d.logger.Dimf("Preparing and verifying Docker credentials...")
170+
171+
// This will retrieve and verify credentials, returning error if invalid
172+
creds, err := d.dockerAuth.GetAndVerifyCredentials()
173+
if err != nil {
174+
return err
175+
}
176+
177+
// Store the verified credentials
178+
d.dockerCreds = creds
179+
180+
d.logger.Dimf("Docker credentials verified successfully")
181+
return nil
182+
}
183+
160184
func (d *Deployer) deployCentral(ctx context.Context, resources, exposure string) error {
161185
if d.namespaceExists(d.centralNamespace) {
162186
d.logger.Info("Existing Central deployment found, tearing down...")

pkg/dockerauth/dockerauth.go

Lines changed: 76 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ const (
1919

2020
// DockerAuth handles Docker authentication and pull secret management.
2121
type DockerAuth struct {
22-
logger *logger.Logger
22+
logger *logger.Logger
23+
skipCredVerification bool
2324
}
2425

2526
// DockerConfig represents Docker configuration structure.
@@ -40,26 +41,33 @@ type CredentialData struct {
4041
Secret string `json:"Secret"`
4142
}
4243

44+
// Credentials represents verified Docker credentials.
45+
type Credentials struct {
46+
Username string
47+
Password string
48+
}
49+
4350
// New creates a new DockerAuth instance.
4451
func New(log *logger.Logger) *DockerAuth {
4552
return &DockerAuth{
4653
logger: log,
4754
}
4855
}
4956

50-
// GetDockerAuthString generates Docker authentication string for image pull secrets
51-
func (d *DockerAuth) GetDockerAuthString(_, _ string) (string, error) {
57+
// GetAndVerifyCredentials retrieves and verifies Docker credentials.
58+
// This should be called early to fail fast if credentials are invalid.
59+
func (d *DockerAuth) GetAndVerifyCredentials() (*Credentials, error) {
5260
var username, password string
5361

5462
// Try environment variables first.
5563
username = os.Getenv("REGISTRY_USERNAME")
5664
password = os.Getenv("REGISTRY_PASSWORD")
5765

5866
if username != "" && password == "" {
59-
return "", errors.New("REGISTRY_USERNAME set but REGISTRY_PASSWORD is empty")
67+
return nil, errors.New("REGISTRY_USERNAME set but REGISTRY_PASSWORD is empty")
6068
}
6169
if username == "" && password != "" {
62-
return "", errors.New("REGISTRY_PASSWORD set but REGISTRY_USERNAME is empty")
70+
return nil, errors.New("REGISTRY_PASSWORD set but REGISTRY_USERNAME is empty")
6371
}
6472

6573
if username == "" {
@@ -70,31 +78,26 @@ func (d *DockerAuth) GetDockerAuthString(_, _ string) (string, error) {
7078
var err error
7179
username, password, err = d.getCredentialsFromDockerConfig(dockerConfigPath)
7280
if err != nil {
73-
return "", err
81+
return nil, err
7482
}
7583
}
7684
}
7785

7886
if username == "" || password == "" {
79-
return "", errors.New("no Docker credentials found")
80-
}
81-
82-
// Create auth string.
83-
authString := fmt.Sprintf("%s:%s", username, password)
84-
encodedAuth := base64.StdEncoding.EncodeToString([]byte(authString))
85-
86-
dockerConfig := DockerConfig{
87-
Auths: map[string]AuthEntry{
88-
acsImageRegistry: {Auth: encodedAuth},
89-
},
87+
return nil, errors.New("no Docker credentials found")
9088
}
9189

92-
jsonData, err := json.Marshal(dockerConfig)
93-
if err != nil {
94-
return "", fmt.Errorf("failed to marshal Docker config: %w", err)
90+
// Verify credentials.
91+
if !d.skipCredVerification {
92+
if err := d.VerifyCredentials(username, password); err != nil {
93+
return nil, fmt.Errorf("credentials are invalid: %w", err)
94+
}
9595
}
9696

97-
return string(jsonData), nil
97+
return &Credentials{
98+
Username: username,
99+
Password: password,
100+
}, nil
98101
}
99102

100103
// getCredentialsFromDockerConfig extracts credentials from existing Docker config.
@@ -172,14 +175,60 @@ func (d *DockerAuth) getCredentialFromHelper(helperName, registry string) (*Cred
172175
return &credData, nil
173176
}
174177

175-
// CreatePullSecretYAML creates Kubernetes pull secret YAML.
176-
func (d *DockerAuth) CreatePullSecretYAML(namespace string) (string, error) {
177-
dockerConfigJSON, err := d.GetDockerAuthString("", "")
178+
// VerifyCredentials attempts to verify that the credentials work by making a request to the registry.
179+
// This uses a read-only HTTP request.
180+
// It mimics what the kubelet would do when pulling images.
181+
func (d *DockerAuth) VerifyCredentials(username, password string) error {
182+
// Create auth header for Basic authentication
183+
authString := fmt.Sprintf("%s:%s", username, password)
184+
encodedAuth := base64.StdEncoding.EncodeToString([]byte(authString))
185+
186+
// Try to get a token from quay.io's OAuth2 endpoint for a specific repository
187+
// This mimics what kubelet does when pulling images - it requests a token with pull scope
188+
// for the specific repository.
189+
repository := "rhacs-eng/main"
190+
authURL := fmt.Sprintf("https://%s/v2/auth?service=%s&scope=repository:%s:pull",
191+
acsImageRegistry, acsImageRegistry, repository)
192+
193+
cmd := exec.Command("curl", "-s", "-f",
194+
"-H", fmt.Sprintf("Authorization: Basic %s", encodedAuth),
195+
authURL)
196+
197+
output, err := cmd.CombinedOutput()
178198
if err != nil {
179-
return "", err
199+
d.logger.Warningf("Failed to verify credentials for %s: %v", acsImageRegistry, err)
200+
d.logger.Dimf("Verification output: %s", string(output))
201+
return fmt.Errorf("credential verification failed for %s: %w", acsImageRegistry, err)
202+
}
203+
204+
// Check if we got a valid JSON response with a token
205+
var tokenResponse map[string]interface{}
206+
if err := json.Unmarshal(output, &tokenResponse); err != nil {
207+
return fmt.Errorf("credential verification failed: invalid response from %s: %w", acsImageRegistry, err)
208+
}
209+
210+
if _, ok := tokenResponse["token"]; !ok {
211+
return fmt.Errorf("credential verification failed: no token received from %s", acsImageRegistry)
212+
}
213+
214+
d.logger.Dimf("Successfully verified credentials for %s (repository: %s)", acsImageRegistry, repository)
215+
return nil
216+
}
217+
218+
// CreatePullSecretYAMLFromCredentials creates Kubernetes pull secret YAML from verified credentials.
219+
func (d *DockerAuth) CreatePullSecretYAMLFromCredentials(creds *Credentials, namespace string) string {
220+
// Create auth string
221+
authString := fmt.Sprintf("%s:%s", creds.Username, creds.Password)
222+
encodedAuth := base64.StdEncoding.EncodeToString([]byte(authString))
223+
224+
dockerConfig := DockerConfig{
225+
Auths: map[string]AuthEntry{
226+
acsImageRegistry: {Auth: encodedAuth},
227+
},
180228
}
181229

182-
encodedConfig := base64.StdEncoding.EncodeToString([]byte(dockerConfigJSON))
230+
jsonData, _ := json.Marshal(dockerConfig)
231+
encodedConfig := base64.StdEncoding.EncodeToString(jsonData)
183232

184233
secretYAML := fmt.Sprintf(`apiVersion: v1
185234
kind: Secret
@@ -191,5 +240,5 @@ data:
191240
.dockerconfigjson: %s
192241
`, namespace, encodedConfig)
193242

194-
return secretYAML, nil
243+
return secretYAML
195244
}

pkg/dockerauth/dockerauth_test.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
"github.com/stackrox/roxie/pkg/logger"
1111
)
1212

13-
func TestCreatePullSecretYAMLFromEnv(t *testing.T) {
13+
func TestGetAndVerifyCredentialsFromEnv(t *testing.T) {
1414
// Set environment variables for test
1515
os.Setenv("REGISTRY_USERNAME", "user")
1616
os.Setenv("REGISTRY_PASSWORD", "pass")
@@ -21,12 +21,23 @@ func TestCreatePullSecretYAMLFromEnv(t *testing.T) {
2121

2222
log := logger.New()
2323
da := New(log)
24+
da.skipCredVerification = true // Skip verification in tests
2425

25-
yamlText, err := da.CreatePullSecretYAML("ns")
26+
creds, err := da.GetAndVerifyCredentials()
2627
if err != nil {
27-
t.Fatalf("CreatePullSecretYAML failed: %v", err)
28+
t.Fatalf("GetAndVerifyCredentials failed: %v", err)
2829
}
2930

31+
if creds.Username != "user" {
32+
t.Errorf("Expected username 'user', got '%s'", creds.Username)
33+
}
34+
if creds.Password != "pass" {
35+
t.Errorf("Expected password 'pass', got '%s'", creds.Password)
36+
}
37+
38+
// Test creating YAML from credentials
39+
yamlText := da.CreatePullSecretYAMLFromCredentials(creds, "ns")
40+
3041
// Verify YAML structure
3142
if !strings.Contains(yamlText, "apiVersion: v1") {
3243
t.Error("YAML should contain 'apiVersion: v1'")
@@ -71,7 +82,7 @@ func TestCreatePullSecretYAMLFromEnv(t *testing.T) {
7182
}
7283
}
7384

74-
func TestCreatePullSecretYAMLNoCredentials(t *testing.T) {
85+
func TestGetAndVerifyCredentialsNoCredentials(t *testing.T) {
7586
// Ensure no credentials are set
7687
os.Unsetenv("REGISTRY_USERNAME")
7788
os.Unsetenv("REGISTRY_PASSWORD")
@@ -95,8 +106,9 @@ func TestCreatePullSecretYAMLNoCredentials(t *testing.T) {
95106

96107
log := logger.New()
97108
da := New(log)
109+
da.skipCredVerification = true // Skip verification in tests
98110

99-
_, err := da.CreatePullSecretYAML("ns")
111+
_, err := da.GetAndVerifyCredentials()
100112
if err == nil {
101113
t.Error("Expected error when no credentials are available")
102114
}

0 commit comments

Comments
 (0)