Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions controllers/quay/quayregistry_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -857,6 +857,17 @@ func (r *QuayRegistryReconciler) Reconcile(ctx context.Context, req ctrl.Request
}
}

if err := kustomize.ValidateProgrammaticBootstrapConfig(usercfg); err != nil {
return r.reconcileWithCondition(
ctx,
&quay,
v1.ConditionTypeRolloutBlocked,
metav1.ConditionTrue,
v1.ConditionReasonConfigInvalid,
err.Error(),
)
}

updatedQuay.Status.Conditions = v1.RemoveCondition(
updatedQuay.Status.Conditions, v1.ConditionTypeRolloutBlocked,
)
Expand Down
34 changes: 34 additions & 0 deletions controllers/quay/quayregistry_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -605,6 +605,40 @@ var _ = Describe("Reconciling a QuayRegistry", func() {
})
})

When("programmatic bootstrap is enabled but BOOTSTRAP_TOKEN_OWNER is missing", func() {
BeforeEach(func() {
quayRegistry = newQuayRegistry("test-registry", namespace)
configBundle = newConfigBundle("quay-config-secret-abc123", namespace, true)
config := map[string]interface{}{}
Expect(yaml.Unmarshal(configBundle.Data["config.yaml"], &config)).To(Succeed())
config[kustomize.ProgrammaticBootstrapFeatureConfigField] = true
configBundle.Data["config.yaml"] = encode(config)
quayRegistry.Spec.ConfigBundleSecret = configBundle.GetName()
quayRegistryName = types.NamespacedName{
Name: quayRegistry.Name,
Namespace: quayRegistry.Namespace,
}

Expect(k8sClient.Create(context.Background(), &configBundle)).Should(Succeed())
Expect(k8sClient.Create(context.Background(), quayRegistry)).Should(Succeed())

result, err = controller.Reconcile(context.Background(), reconcile.Request{NamespacedName: quayRegistryName})
})

It("does not return an error", func() {
Expect(err).NotTo(HaveOccurred())
})

It("sets a RolloutBlocked condition with ConfigInvalid reason", func() {
var updatedQuay v1.QuayRegistry
Expect(k8sClient.Get(context.Background(), quayRegistryName, &updatedQuay)).To(Succeed())
cond := v1.GetCondition(updatedQuay.Status.Conditions, v1.ConditionTypeRolloutBlocked)
Expect(cond).NotTo(BeNil())
Expect(string(cond.Reason)).To(Equal(string(v1.ConditionReasonConfigInvalid)))
Expect(cond.Message).To(ContainSubstring(kustomize.BootstrapTokenOwnerConfigField))
})
})

When("the current version in the `status` block is the same as the Operator", func() {
BeforeEach(func() {
quayRegistry = newQuayRegistry("test-registry", namespace)
Expand Down
18 changes: 18 additions & 0 deletions pkg/kustomize/kustomize.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const (
ProgrammaticTokenK8sSecretConfigField = "PROGRAMMATIC_TOKEN_K8S_SECRET"
ProgrammaticTokenK8sKeyConfigField = "PROGRAMMATIC_TOKEN_K8S_KEY"
ProgrammaticTokenPathConfigField = "PROGRAMMATIC_TOKEN_PATH"
BootstrapTokenOwnerConfigField = "BOOTSTRAP_TOKEN_OWNER"
BootstrapTokenSecretKey = "token.json"
BootstrapTokenMountPath = "/var/lib/quay/bootstrap-token"
BootstrapTokenConfigPath = BootstrapTokenMountPath + "/" + BootstrapTokenSecretKey
Expand Down Expand Up @@ -197,6 +198,23 @@ func ProgrammaticBootstrapEnabled(config map[string]interface{}) bool {
return ok && enabled
}

// ValidateProgrammaticBootstrapConfig checks that required configuration keys
// are present when FEATURE_PROGRAMMATIC_BOOTSTRAP is enabled.
func ValidateProgrammaticBootstrapConfig(config map[string]interface{}) error {
if !ProgrammaticBootstrapEnabled(config) {
return nil
}
owner, ok := config[BootstrapTokenOwnerConfigField]
if !ok {
return fmt.Errorf("%s must be set when %s is enabled", BootstrapTokenOwnerConfigField, ProgrammaticBootstrapFeatureConfigField)
}
ownerStr, ok := owner.(string)
if !ok || ownerStr == "" {
return fmt.Errorf("%s must be a non-empty string", BootstrapTokenOwnerConfigField)
}
return nil
}

func injectProgrammaticBootstrapTokenConfig(quay *v1.QuayRegistry, config map[string]interface{}) {
config[ProgrammaticTokenK8sSecretConfigField] = BootstrapTokenSecretName(quay)
config[ProgrammaticTokenK8sKeyConfigField] = BootstrapTokenSecretKey
Expand Down
92 changes: 92 additions & 0 deletions pkg/kustomize/kustomize_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1209,6 +1209,98 @@ func TestInflateProgrammaticBootstrapTokenOverridesConflictingConfig(t *testing.
assert.Equal(t, BootstrapTokenConfigPath, config[ProgrammaticTokenPathConfigField])
}

func TestValidateProgrammaticBootstrapConfig(t *testing.T) {
tests := []struct {
name string
config map[string]interface{}
wantErr bool
}{
{
name: "feature disabled",
config: map[string]interface{}{ProgrammaticBootstrapFeatureConfigField: false},
wantErr: false,
},
{
name: "feature absent",
config: map[string]interface{}{},
wantErr: false,
},
{
name: "feature enabled with owner set",
config: map[string]interface{}{
ProgrammaticBootstrapFeatureConfigField: true,
BootstrapTokenOwnerConfigField: "admin",
},
wantErr: false,
},
{
name: "feature enabled without owner",
config: map[string]interface{}{
ProgrammaticBootstrapFeatureConfigField: true,
},
wantErr: true,
},
{
name: "feature enabled with empty owner",
config: map[string]interface{}{
ProgrammaticBootstrapFeatureConfigField: true,
BootstrapTokenOwnerConfigField: "",
},
wantErr: true,
},
{
name: "feature enabled with non-string owner",
config: map[string]interface{}{
ProgrammaticBootstrapFeatureConfigField: true,
BootstrapTokenOwnerConfigField: 42,
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateProgrammaticBootstrapConfig(tt.config)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}

func TestInflateProgrammaticBootstrapTokenPreservesUserConfig(t *testing.T) {
log := testlogr.NewTestLogger(t)
ctx := quaycontext.QuayRegistryContext{
DbUri: "postgresql://user:pass@db:5432/db",
}
quay := &v1.QuayRegistry{
ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "test-ns"},
Spec: v1.QuayRegistrySpec{},
Status: v1.QuayRegistryStatus{CurrentVersion: v1.QuayVersionCurrent},
}
configBundle := &corev1.Secret{
Data: map[string][]byte{
"config.yaml": encode(map[string]interface{}{
"SERVER_HOSTNAME": "quay.io",
"DB_URI": ctx.DbUri,
ProgrammaticBootstrapFeatureConfigField: true,
BootstrapTokenOwnerConfigField: "bootstrap-admin",
"BOOTSTRAP_TOKEN_EXPIRATION": 3600,
"BOOTSTRAP_TOKEN_SCOPE": "org:admin",
}),
},
}

pieces, err := Inflate(&ctx, quay, configBundle, log, false)
require.NoError(t, err)

config := renderedQuayConfig(t, pieces)
assert.Equal(t, "bootstrap-admin", config[BootstrapTokenOwnerConfigField])
assert.Equal(t, 3600, int(config["BOOTSTRAP_TOKEN_EXPIRATION"].(float64)))
assert.Equal(t, "org:admin", config["BOOTSTRAP_TOKEN_SCOPE"])
}

func renderedQuayConfig(t *testing.T, pieces []client.Object) map[string]interface{} {
t.Helper()
for _, obj := range pieces {
Expand Down
Loading