diff --git a/apis/quay/v1/quayregistry_types.go b/apis/quay/v1/quayregistry_types.go index 7249e5dfd..e03fc326b 100644 --- a/apis/quay/v1/quayregistry_types.go +++ b/apis/quay/v1/quayregistry_types.go @@ -37,16 +37,16 @@ var QuayVersionCurrent QuayVersion = QuayVersion(os.Getenv("QUAY_VERSION")) type ComponentKind string const ( - ComponentBase ComponentKind = "base" - ComponentPostgres = "postgres" - ComponentClair = "clair" - ComponentRedis = "redis" - ComponentHPA = "horizontalpodautoscaler" - ComponentObjectStorage = "objectstorage" - ComponentRoute = "route" - ComponentMirror = "mirror" - ComponentMonitoring = "monitoring" - ComponentTLS = "tls" + ComponentBase = "base" + ComponentPostgres = "postgres" + ComponentClair = "clair" + ComponentRedis = "redis" + ComponentHPA = "horizontalpodautoscaler" + ComponentObjectStorage = "objectstorage" + ComponentRoute = "route" + ComponentMirror = "mirror" + ComponentMonitoring = "monitoring" + ComponentTLS = "tls" ) var allComponents = []ComponentKind{ @@ -68,6 +68,19 @@ var requiredComponents = []ComponentKind{ ComponentRedis, } +// fixme (jonathankingfc) - These should not be strings, use a correct type +var overrideSupport = map[ComponentKind][]string{ + ComponentPostgres: {"volumeSize", "labels", "env"}, + ComponentClair: {"volumeSize", "labels", "env"}, + ComponentRedis: {"labels", "env"}, + ComponentHPA: {"labels"}, + ComponentObjectStorage: {"volumeSize", "labels", "env"}, + ComponentRoute: {"labels"}, + ComponentMirror: {"labels", "env"}, + ComponentMonitoring: {"labels"}, + ComponentTLS: {"labels"}, +} + const ( ManagedKeysName = "quay-registry-managed-secret-keys" QuayConfigTLSSecretName = "quay-config-tls" @@ -88,6 +101,18 @@ type Component struct { // Managed indicates whether or not the Operator is responsible for the lifecycle of this component. // Default is true. Managed bool `json:"managed"` + // Overrides holds information regarding component specific configurations. + Overrides Override `json:"overrides,omitempty"` +} + +// Override describes configuration overrides for the given managed component +type Override struct { + // Env holds environment variable overrides that will be added to the given managed components deployment (if available) + Env map[string]string `json:"env,omitempty"` + // Labels holds labels that will be added to the given managed components resources (if available) + Labels map[string]string `json:"labels,omitempty"` + // VolumeSize holds a volume size override that will be set on the given managed components PVC. + VolumeSize string `json:"volumeSize,omitempty"` } type ConditionType string @@ -127,6 +152,7 @@ const ( ConditionReasonObjectStorageComponentDependencyError ConditionReason = "ObjectStorageComponentDependencyError" ConditionReasonMonitoringComponentDependencyError ConditionReason = "MonitoringComponentDependencyError" ConditionReasonConfigInvalid ConditionReason = "ConfigInvalid" + ConditionReasonInvalidComponentOverride ConditionReason = "ComponentOverrideInvalid" ) // Condition is a single condition of a QuayRegistry. @@ -330,6 +356,34 @@ func EnsureDefaultComponents(ctx *quaycontext.QuayRegistryContext, quay *QuayReg return updatedQuay, nil } +// ValidateOverrides validates that the overrides set for each component are valid. +func ValidateOverrides(quay *QuayRegistry) error { + for _, component := range quay.Spec.Components { + + // If the component is unmanaged, we cannot set overrides + if !ComponentIsManaged(quay.Spec.Components, component.Kind) { + if component.Overrides.Env != nil || component.Overrides.Labels != nil || component.Overrides.VolumeSize != "" { + return errors.New("cannot set overrides on unmanaged component " + string(component.Kind)) + } + } + + // Validate fields + if component.Overrides.Env != nil && !ComponentSupportsOverride(component.Kind, "env") { + return fmt.Errorf("component %s does not support env overrides", component.Kind) + } + if component.Overrides.Labels != nil && !ComponentSupportsOverride(component.Kind, "labels") { + return fmt.Errorf("component %s does not support label overrides", component.Kind) + } + if component.Overrides.VolumeSize != "" && !ComponentSupportsOverride(component.Kind, "volumeSize") { + return fmt.Errorf("component %s does not support volumeSize overrides", component.Kind) + } + + } + + return nil + +} + // EnsureRegistryEndpoint sets the `status.registryEndpoint` field and returns `ok` if it was unchanged. func EnsureRegistryEndpoint(ctx *quaycontext.QuayRegistryContext, quay *QuayRegistry, config map[string]interface{}) (*QuayRegistry, bool) { updatedQuay := quay.DeepCopy() @@ -493,6 +547,15 @@ func FieldGroupNamesForManagedComponents(quay *QuayRegistry) ([]string, error) { return fgns, nil } +func ComponentSupportsOverride(component ComponentKind, override string) bool { + for _, supportedOverride := range overrideSupport[component] { + if override == supportedOverride { + return true + } + } + return false +} + func init() { SchemeBuilder.Register(&QuayRegistry{}, &QuayRegistryList{}) } diff --git a/apis/quay/v1/quayregistry_types_test.go b/apis/quay/v1/quayregistry_types_test.go index cf646ed13..af513ab71 100644 --- a/apis/quay/v1/quayregistry_types_test.go +++ b/apis/quay/v1/quayregistry_types_test.go @@ -351,6 +351,115 @@ var componentsMatchTests = []struct { }, } +var validateOverridesTests = []struct { + name string + quay QuayRegistry + expectedErr error +}{ + { + "NoOverridesProvided", + QuayRegistry{ + Spec: QuayRegistrySpec{ + Components: []Component{ + {Kind: "postgres", Managed: true}, + {Kind: "redis", Managed: true}, + {Kind: "clair", Managed: true}, + {Kind: "objectstorage", Managed: true}, + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true}, + {Kind: "horizontalpodautoscaler", Managed: true}, + {Kind: "mirror", Managed: true}, + {Kind: "monitoring", Managed: true}, + }, + }, + }, + nil, + }, + { + "InvalidVolumeSizeOverride", + QuayRegistry{ + Spec: QuayRegistrySpec{ + Components: []Component{ + {Kind: "postgres", Managed: true}, + {Kind: "redis", Managed: true}, + {Kind: "clair", Managed: true}, + {Kind: "objectstorage", Managed: true}, + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true, Overrides: Override{VolumeSize: "50Gb"}}, + {Kind: "horizontalpodautoscaler", Managed: true}, + {Kind: "mirror", Managed: true}, + {Kind: "monitoring", Managed: true}, + }, + }, + }, + errors.New("component tls does not support volumeSize overrides"), + }, + { + "InvalidEnvOverride", + QuayRegistry{ + Spec: QuayRegistrySpec{ + Components: []Component{ + {Kind: "postgres", Managed: true}, + {Kind: "redis", Managed: true}, + {Kind: "clair", Managed: true}, + {Kind: "objectstorage", Managed: true}, + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true, Overrides: Override{Env: map[string]string{"DEBUGLOGS": "true"}}}, + {Kind: "horizontalpodautoscaler", Managed: true}, + {Kind: "mirror", Managed: true}, + {Kind: "monitoring", Managed: true}, + }, + }, + }, + errors.New("component tls does not support env overrides"), + }, + { + "AllValidOverrides", + QuayRegistry{ + Spec: QuayRegistrySpec{ + Components: []Component{ + {Kind: "postgres", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}, VolumeSize: "50Gb"}}, + {Kind: "redis", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}}}, + {Kind: "clair", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}, VolumeSize: "50Gb"}}, + {Kind: "objectstorage", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}, VolumeSize: "50Gb"}}, + {Kind: "route", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}}}, + {Kind: "tls", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}}}, + {Kind: "horizontalpodautoscaler", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}}}, + {Kind: "mirror", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}}}, + {Kind: "monitoring", Managed: true, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}}}, + }, + }, + }, + nil, + }, + { + "InvalidOverrideUnmanagedComponent", + QuayRegistry{ + Spec: QuayRegistrySpec{ + Components: []Component{ + {Kind: "postgres", Managed: false, Overrides: Override{Labels: map[string]string{"test-label": "test-value"}, Env: map[string]string{"TESTVAR": "TESTAVL"}, VolumeSize: "50Gb"}}, + }, + }, + }, + errors.New("cannot set overrides on unmanaged component postgres"), + }, +} + +func TestValidOverrides(t *testing.T) { + assert := assert.New(t) + + for _, test := range validateOverridesTests { + err := ValidateOverrides(&test.quay) + + if test.expectedErr != nil { + assert.NotNil(err, test.name) + assert.Equal(test.expectedErr, err) + } else { + assert.Equal(test.expectedErr, err) + } + } +} + func TestComponentsMatch(t *testing.T) { assert := assert.New(t) diff --git a/config/crd/bases/quay.redhat.com_quayregistries.yaml b/config/crd/bases/quay.redhat.com_quayregistries.yaml index 4fb816434..5a11e0680 100644 --- a/config/crd/bases/quay.redhat.com_quayregistries.yaml +++ b/config/crd/bases/quay.redhat.com_quayregistries.yaml @@ -43,6 +43,23 @@ spec: managed: description: Managed indicates whether or not the Operator is responsible for the lifecycle of this component. Default is true. type: boolean + overrides: + description: Overrides holds information regarding component specific configurations. + properties: + env: + additionalProperties: + type: string + description: Env holds environment variable overrides that will be added to the given managed components deployment (if available) + type: object + labels: + additionalProperties: + type: string + description: Labels holds labels that will be added to the given managed components resources (if available) + type: object + volumeSize: + description: VolumeSize holds a volume size override that will be set on the given managed components PVC. + type: string + type: object required: - kind - managed diff --git a/controllers/quay/quayregistry_controller.go b/controllers/quay/quayregistry_controller.go index 3b18081c8..a68bc89eb 100644 --- a/controllers/quay/quayregistry_controller.go +++ b/controllers/quay/quayregistry_controller.go @@ -222,6 +222,13 @@ func (r *QuayRegistryReconciler) Reconcile(ctx context.Context, req ctrl.Request return ctrl.Result{}, nil } + err = v1.ValidateOverrides(updatedQuay) + if err != nil { + msg := fmt.Sprintf("could not validate overrides on spec.components: %s", err) + + return r.reconcileWithCondition(&quay, v1.ConditionTypeRolloutBlocked, metav1.ConditionTrue, v1.ConditionReasonInvalidComponentOverride, msg) + } + if !v1.ComponentsMatch(quay.Spec.Components, updatedQuay.Spec.Components) { log.Info("updating QuayRegistry `spec.components` to include defaults") if err = r.Client.Update(ctx, updatedQuay); err != nil { diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 5d951e9ed..7932358d0 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -7,6 +7,7 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -59,8 +60,22 @@ func Process(ctx *quaycontext.QuayRegistryContext, quay *v1.QuayRegistry, obj cl return dep, nil } + // Set env variable overrides + var envVars map[string]string + switch quayComponentLabel { + case "postgres": + envVars = getEnvOverrideForComponent(quay, v1.ComponentPostgres) + case "clair-postgres": + envVars = getEnvOverrideForComponent(quay, v1.ComponentClair) + case "objectstorage": + envVars = getEnvOverrideForComponent(quay, v1.ComponentObjectStorage) + } + for key, val := range envVars { + dep.Spec.Template.Spec.Containers[0].Env = append(dep.Spec.Template.Spec.Containers[0].Env, corev1.EnvVar{Name: key, Value: val}) + } + if !strings.Contains(dep.GetName(), "quay-config-editor") { - return obj, nil + return dep, nil } fgns, err := v1.FieldGroupNamesForManagedComponents(quay) @@ -72,6 +87,21 @@ func Process(ctx *quaycontext.QuayRegistryContext, quay *v1.QuayRegistry, obj cl return dep, nil } + // If the current object is a PVC, check for volume override + if pvc, ok := obj.(*corev1.PersistentVolumeClaim); ok { + var volumeSize resource.Quantity + switch quayComponentLabel { + case "postgres": + volumeSize = getVolumeSizeOverrideForComponent(quay, v1.ComponentPostgres) + case "clair-postgres": + volumeSize = getVolumeSizeOverrideForComponent(quay, v1.ComponentClair) + case "objectstorage": + volumeSize = getVolumeSizeOverrideForComponent(quay, v1.ComponentObjectStorage) + } + pvc.Spec.Resources.Requests = corev1.ResourceList{corev1.ResourceStorage: volumeSize} + return pvc, nil + } + rt, ok := obj.(*route.Route) if !ok { return obj, nil @@ -137,3 +167,27 @@ func FlattenSecret(configBundle *corev1.Secret) (*corev1.Secret, error) { return flattenedSecret, nil } + +func getVolumeSizeOverrideForComponent(quay *v1.QuayRegistry, componentKind v1.ComponentKind) resource.Quantity { + for _, component := range quay.Spec.Components { + if component.Kind == componentKind { + volumeSizeOverride := component.Overrides.VolumeSize + if volumeSizeOverride != "" { + return resource.MustParse(volumeSizeOverride) + } + } + } + return resource.MustParse("50Gi") +} + +func getEnvOverrideForComponent(quay *v1.QuayRegistry, componentKind v1.ComponentKind) map[string]string { + for _, component := range quay.Spec.Components { + if component.Kind == componentKind { + envOverrides := component.Overrides.Env + if envOverrides != nil { + return envOverrides + } + } + } + return map[string]string{} +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index a2f932b07..f1e95845b 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -5,7 +5,9 @@ import ( route "github.com/openshift/api/route/v1" "github.com/stretchr/testify/assert" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" @@ -138,6 +140,113 @@ var processTests = []struct { }, nil, }, + { + "volumeSizeDefault", + &quaycontext.QuayRegistryContext{}, + &v1.QuayRegistry{ + Spec: v1.QuayRegistrySpec{ + Components: []v1.Component{ + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true}, + }, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "postgres"}, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "postgres"}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("50Gi")}}, + }, + }, + nil, + }, + { + "volumeSizeOverridePostgres", + &quaycontext.QuayRegistryContext{}, + &v1.QuayRegistry{ + Spec: v1.QuayRegistrySpec{ + Components: []v1.Component{ + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true}, + {Kind: "postgres", Managed: true, Overrides: v1.Override{VolumeSize: "60Gi"}}, + }, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "postgres"}, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "postgres"}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("60Gi")}}, + }, + }, + nil, + }, + { + "volumeSizeOverrideClairPostgres", + &quaycontext.QuayRegistryContext{}, + &v1.QuayRegistry{ + Spec: v1.QuayRegistrySpec{ + Components: []v1.Component{ + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true}, + {Kind: "postgres", Managed: true, Overrides: v1.Override{VolumeSize: "70Gi"}}, + {Kind: "clair", Managed: true, Overrides: v1.Override{VolumeSize: "40Gi"}}, + }, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "clair-postgres"}, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "clair-postgres"}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("40Gi")}}, + }, + }, + nil, + }, + { + "envOverrideClairApp", + &quaycontext.QuayRegistryContext{}, + &v1.QuayRegistry{ + Spec: v1.QuayRegistrySpec{ + Components: []v1.Component{ + {Kind: "route", Managed: true}, + {Kind: "tls", Managed: true}, + {Kind: "clair", Managed: true, Overrides: v1.Override{Env: map[string]string{"CLAIRVAR": "VAL"}}}, + }, + }, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "clair-postgres"}, + }, + Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{Containers: []corev1.Container{{}}}}}, + }, + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"quay-component": "clair-postgres"}, + }, + Spec: appsv1.DeploymentSpec{Template: corev1.PodTemplateSpec{Spec: corev1.PodSpec{Containers: []corev1.Container{{Env: []corev1.EnvVar{{Name: "CLAIRVAR", Value: "VAL"}}}}}}}, + }, + nil, + }, } func TestProcess(t *testing.T) {