diff --git a/apis/quay/v1/quayregistry_types.go b/apis/quay/v1/quayregistry_types.go index 7249e5dfd..d79c25cc3 100644 --- a/apis/quay/v1/quayregistry_types.go +++ b/apis/quay/v1/quayregistry_types.go @@ -24,6 +24,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -38,15 +39,15 @@ 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" + ComponentPostgres ComponentKind = "postgres" + ComponentClair ComponentKind = "clair" + ComponentRedis ComponentKind = "redis" + ComponentHPA ComponentKind = "horizontalpodautoscaler" + ComponentObjectStorage ComponentKind = "objectstorage" + ComponentRoute ComponentKind = "route" + ComponentMirror ComponentKind = "mirror" + ComponentMonitoring ComponentKind = "monitoring" + ComponentTLS ComponentKind = "tls" ) var allComponents = []ComponentKind{ @@ -68,6 +69,11 @@ var requiredComponents = []ComponentKind{ ComponentRedis, } +var supportsVolumeOverride = []ComponentKind{ + ComponentPostgres, + ComponentClair, +} + const ( ManagedKeysName = "quay-registry-managed-secret-keys" QuayConfigTLSSecretName = "quay-config-tls" @@ -88,6 +94,13 @@ 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 { + VolumeSize *resource.Quantity `json:"volumeSize,omitempty"` } type ConditionType string @@ -127,6 +140,7 @@ const ( ConditionReasonObjectStorageComponentDependencyError ConditionReason = "ObjectStorageComponentDependencyError" ConditionReasonMonitoringComponentDependencyError ConditionReason = "MonitoringComponentDependencyError" ConditionReasonConfigInvalid ConditionReason = "ConfigInvalid" + ConditionReasonComponentOverrideInvalid ConditionReason = "ComponentOverrideInvalid" ) // Condition is a single condition of a QuayRegistry. @@ -330,6 +344,33 @@ 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 { + + // No overrides provided + if component.Overrides == nil { + continue + } + + // If the component is unmanaged, we cannot set overrides + if !ComponentIsManaged(quay.Spec.Components, component.Kind) { + if component.Overrides.VolumeSize != nil { + return errors.New("cannot set overrides on unmanaged component " + string(component.Kind)) + } + } + + // Check that component supports override + if component.Overrides.VolumeSize != nil && !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 +534,24 @@ func FieldGroupNamesForManagedComponents(quay *QuayRegistry) ([]string, error) { return fgns, nil } +// ComponentSupportsOverride returns whether or not a given component supports the given override. +func ComponentSupportsOverride(component ComponentKind, override string) bool { + + // Using a switch statement for possible implementation of future overrides + switch override { + case "volumeSize": + for _, c := range supportsVolumeOverride { + if c == component { + return true + } + } + default: + return false + } + + 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..1d80b5acb 100644 --- a/apis/quay/v1/quayregistry_types_test.go +++ b/apis/quay/v1/quayregistry_types_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" quaycontext "github.com/quay/quay-operator/pkg/context" @@ -351,6 +352,67 @@ 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: &resource.Quantity{}}}, + {Kind: "horizontalpodautoscaler", Managed: true}, + {Kind: "mirror", Managed: true}, + {Kind: "monitoring", Managed: true}, + }, + }, + }, + errors.New("component tls does not support volumeSize overrides"), + }, +} + +func TestValidOverrides(t *testing.T) { + assert := assert.New(t) + + for _, test := range validateOverridesTests { + t.Run(test.name, func(t *testing.T) { + 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..ab71a7962 100644 --- a/config/crd/bases/quay.redhat.com_quayregistries.yaml +++ b/config/crd/bases/quay.redhat.com_quayregistries.yaml @@ -43,6 +43,16 @@ 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: + volumeSize: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object required: - kind - managed diff --git a/controllers/quay/quayregistry_controller.go b/controllers/quay/quayregistry_controller.go index 3b18081c8..5f11471cf 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.ConditionReasonComponentOverrideInvalid, 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..cb8b4193f 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,12 +1,14 @@ package middleware import ( + "fmt" "strings" route "github.com/openshift/api/route/v1" 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" @@ -60,7 +62,7 @@ func Process(ctx *quaycontext.QuayRegistryContext, quay *v1.QuayRegistry, obj cl } if !strings.Contains(dep.GetName(), "quay-config-editor") { - return obj, nil + return dep, nil } fgns, err := v1.FieldGroupNamesForManagedComponents(quay) @@ -72,6 +74,28 @@ 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 volumeSizeOverride *resource.Quantity + switch quayComponentLabel { + case "postgres": + volumeSizeOverride = getVolumeSizeOverrideForComponent(quay, v1.ComponentPostgres) + case "clair-postgres": + volumeSizeOverride = getVolumeSizeOverrideForComponent(quay, v1.ComponentClair) + } + + // If override was provided + if volumeSizeOverride != nil { + // Ensure that volume size is not being reduced + if pvc.Spec.Resources.Requests.Storage() != nil && volumeSizeOverride.Cmp(*pvc.Spec.Resources.Requests.Storage()) == -1 { + return nil, fmt.Errorf("cannot shrink volume override size from %s to %s", pvc.Spec.Resources.Requests.Storage().String(), volumeSizeOverride.String()) + } + pvc.Spec.Resources.Requests = corev1.ResourceList{corev1.ResourceStorage: *volumeSizeOverride} + } + + return pvc, nil + } + rt, ok := obj.(*route.Route) if !ok { return obj, nil @@ -137,3 +161,14 @@ func FlattenSecret(configBundle *corev1.Secret) (*corev1.Secret, error) { return flattenedSecret, nil } + +func getVolumeSizeOverrideForComponent(quay *v1.QuayRegistry, componentKind v1.ComponentKind) (volumeSizeOverride *resource.Quantity) { + for _, component := range quay.Spec.Components { + if component.Kind == componentKind { + if component.Overrides != nil && component.Overrides.VolumeSize != nil { + volumeSizeOverride = component.Overrides.VolumeSize + } + } + } + return volumeSizeOverride +} diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index a2f932b07..84ec0b762 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -1,11 +1,13 @@ package middleware import ( + "fmt" "testing" route "github.com/openshift/api/route/v1" "github.com/stretchr/testify/assert" 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,20 +140,133 @@ 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"}, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + Resources: corev1.ResourceRequirements{Requests: corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("50Gi")}}, + }, + }, + &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: parseResourceString("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: parseResourceString("70Gi")}}, + {Kind: "clair", Managed: true, Overrides: &v1.Override{VolumeSize: parseResourceString("60Gi")}}, + }, + }, + }, + &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("60Gi")}}, + }, + }, + nil, + }, + { + "volumeSizeShrinkError", + &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: parseResourceString("30Gi")}}, + }, + }, + }, + &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, + fmt.Errorf("cannot shrink volume override size from 50Gi to 30Gi"), + }, } func TestProcess(t *testing.T) { assert := assert.New(t) - for _, test := range processTests { - processedObj, err := Process(test.ctx, test.quay, test.obj) - if test.expectedError != nil { - assert.Error(err, test.name) - } else { - assert.Nil(err, test.name) - } + t.Run(test.name, func(t *testing.T) { + processedObj, err := Process(test.ctx, test.quay, test.obj) + if test.expectedError != nil { + assert.Error(err, test.name) + } else { + assert.Nil(err, test.name) + } + assert.Equal(test.expected, processedObj, test.name) + }) - assert.Equal(test.expected, processedObj, test.name) } } + +func parseResourceString(s string) *resource.Quantity { + resourceSize := resource.MustParse(s) + return &resourceSize +}