Skip to content

operator: support customized storage class name (PROJQUAY-4141) #1045

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jun 25, 2025
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
44 changes: 33 additions & 11 deletions apis/quay/v1/quayregistry_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ var supportsVolumeOverride = []ComponentKind{
ComponentClairPostgres,
}

var supportsStorageClassOverride = []ComponentKind{
ComponentPostgres,
ComponentClairPostgres,
}

var supportsEnvOverride = []ComponentKind{
ComponentQuay,
ComponentClair,
Expand Down Expand Up @@ -148,7 +153,9 @@ type Component struct {
// Override describes configuration overrides for the given managed component
type Override struct {
VolumeSize *resource.Quantity `json:"volumeSize,omitempty"`
Env []corev1.EnvVar `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// StorageClassName is the name of the StorageClass to use for the PVC.
StorageClassName *string `json:"storageClassName,omitempty"`
Env []corev1.EnvVar `json:"env,omitempty" patchStrategy:"merge" patchMergeKey:"name"`
// +nullable
Replicas *int32 `json:"replicas,omitempty"`
Affinity *corev1.Affinity `json:"affinity,omitempty"`
Expand Down Expand Up @@ -209,6 +216,8 @@ const (
ConditionReasonMonitoringComponentDependencyError ConditionReason = "MonitoringComponentDependencyError"
ConditionReasonConfigInvalid ConditionReason = "ConfigInvalid"
ConditionReasonComponentOverrideInvalid ConditionReason = "ComponentOverrideInvalid"
ConditionReasonPVCPending ConditionReason = "PVCPending"
ConditionReasonPVCProvisioningFailed ConditionReason = "PVCProvisioningFailed"
)

// Condition is a single condition of a QuayRegistry.
Expand Down Expand Up @@ -509,6 +518,7 @@ func ValidateOverrides(quay *QuayRegistry) error {

hasaffinity := hasAffinity(component)
hasvolume := component.Overrides.VolumeSize != nil
hasstorageclass := component.Overrides.StorageClassName != nil
hasreplicas := component.Overrides.Replicas != nil
hasresources := component.Overrides.Resources != nil
hasenvvar := len(component.Overrides.Env) > 0
Expand Down Expand Up @@ -541,6 +551,13 @@ func ValidateOverrides(quay *QuayRegistry) error {
)
}

if hasstorageclass && !ComponentSupportsOverride(component.Kind, "storageClassName") {
return fmt.Errorf(
"component %s does not support storageClassName overrides",
component.Kind,
)
}

if hasenvvar && !ComponentSupportsOverride(component.Kind, "env") {
return fmt.Errorf(
"component %s does not support env overrides",
Expand Down Expand Up @@ -734,6 +751,8 @@ func ComponentSupportsOverride(component ComponentKind, override string) bool {
switch override {
case "volumeSize":
components = supportsVolumeOverride
case "storageClassName":
components = supportsStorageClassOverride
case "env":
components = supportsEnvOverride
case "replicas":
Expand Down Expand Up @@ -777,26 +796,29 @@ func GetReplicasOverrideForComponent(quay *QuayRegistry, kind ComponentKind) *in
return nil
}

// GetVolumeSizeOverrideForComponent returns the volume size overrides set by the user for the
// provided component. Returns nil if not set.
// GetVolumeSizeOverrideForComponent returns the volume size override for a given component kind.
func GetVolumeSizeOverrideForComponent(
quay *QuayRegistry, kind ComponentKind,
) (qt *resource.Quantity) {
for _, component := range quay.Spec.Components {
if component.Kind != kind {
continue
if component.Kind == kind && component.Overrides != nil && component.Overrides.VolumeSize != nil {
return component.Overrides.VolumeSize
}
}
return nil
}

if component.Overrides != nil && component.Overrides.VolumeSize != nil {
qt = component.Overrides.VolumeSize
// GetStorageClassNameOverrideForComponent returns the StorageClass override for a given component kind.
func GetStorageClassNameOverrideForComponent(quay *QuayRegistry, kind ComponentKind) *string {
for _, component := range quay.Spec.Components {
if component.Kind == kind && component.Overrides != nil && component.Overrides.StorageClassName != nil {
return component.Overrides.StorageClassName
}
return
}
return
return nil
}

// GetResourceOverridesForComponent returns the resource overrides set by the user for the
// provided component. Returns nil if not set.
// GetResourceOverridesForComponent returns the resource overrides for a given component kind.
func GetResourceOverridesForComponent(
quay *QuayRegistry, kind ComponentKind,
) (resources *Resources) {
Expand Down
20 changes: 20 additions & 0 deletions apis/quay/v1/quayregistry_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,26 @@ var validateOverridesTests = []struct {
},
errors.New("component postgres does not support affinity overrides"),
},
{
"InvalidStorageClassNameOverride",
QuayRegistry{
Spec: QuayRegistrySpec{
Components: []Component{
{Kind: "postgres", Managed: true},
{Kind: "clairpostgres", Managed: true},
{Kind: "redis", Managed: true, Overrides: &Override{StorageClassName: ptr.To("foo")}},
{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},
},
},
},
errors.New("component redis does not support storageClassName overrides"),
},
}

func TestValidOverrides(t *testing.T) {
Expand Down
5 changes: 5 additions & 0 deletions apis/quay/v1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions bundle/manifests/quayregistries.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,10 @@ spec:
quantity) pairs.
type: object
type: object
storageClassName:
description: StorageClassName is the name of the StorageClass
to use for the PVC.
type: string
volumeSize:
anyOf:
- type: integer
Expand Down
4 changes: 4 additions & 0 deletions config/crd/bases/quay.redhat.com_quayregistries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -980,6 +980,10 @@ spec:
quantity) pairs.
type: object
type: object
storageClassName:
description: StorageClassName is the name of the StorageClass
to use for the PVC.
type: string
volumeSize:
anyOf:
- type: integer
Expand Down
24 changes: 24 additions & 0 deletions config/samples/overrides.quayregistry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
apiVersion: quay.redhat.com/v1
kind: QuayRegistry
metadata:
name: registry
namespace: quay
spec:
configBundleSecret: config-bundle-secret
components:
- kind: objectstorage
managed: false
- kind: route
managed: false
- kind: tls
managed: false
- kind: clair
managed: false
- kind: postgres
managed: true
overrides:
storageClassName: "local-path"
- kind: clairpostgres
managed: true
overrides:
storageClassName: "local-path"
2 changes: 1 addition & 1 deletion controllers/quay/quayregistry_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -753,7 +753,7 @@ func (r *QuayRegistryReconciler) Reconcile(ctx context.Context, req ctrl.Request
v1.ConditionTypeRolloutBlocked,
metav1.ConditionTrue,
v1.ConditionReasonComponentCreationFailed,
fmt.Sprintf("error creating object: %s", err),
fmt.Sprintf("error creating/updating object %s %s: %s", obj.GetObjectKind().GroupVersionKind().Kind, obj.GetName(), err.Error()),
)
}
}
Expand Down
2 changes: 1 addition & 1 deletion e2e/resource_overrides/00-assert.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ spec:
memory: 300Mi
requests:
cpu: 300m
memory: 300Mi
memory: 300Mi
2 changes: 1 addition & 1 deletion e2e/resource_overrides/00-create-quay-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@ spec:
memory: 300Mi
requests:
cpu: 300m
memory: 300Mi
memory: 300Mi
13 changes: 13 additions & 0 deletions e2e/storageclass_overrides/00-assert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: skynet-clair-postgres-15
spec:
storageClassName: local-path
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: skynet-quay-postgres-13
spec:
storageClassName: local-path
15 changes: 15 additions & 0 deletions e2e/storageclass_overrides/00-create-quay-registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
apiVersion: quay.redhat.com/v1
kind: QuayRegistry
metadata:
name: skynet
spec:
configBundleSecret: config-bundle-secret
components:
- kind: postgres
managed: true
overrides:
storageClassName: local-path
- kind: clairpostgres
managed: true
overrides:
storageClassName: local-path
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ require (
github.com/tidwall/sjson v1.2.3
go.uber.org/zap v1.25.0
golang.org/x/net v0.24.0
golang.org/x/text v0.14.0
gopkg.in/yaml.v2 v2.4.0
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.28.3
Expand Down Expand Up @@ -111,7 +112,6 @@ require (
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.20.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
Expand Down
17 changes: 17 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ limitations under the License.
package main

import (
"context"
"crypto/tls"
"flag"
"os"
Expand All @@ -36,6 +37,7 @@ import (

quay "github.com/quay/quay-operator/apis/quay/v1"
quaycontroller "github.com/quay/quay-operator/controllers/quay"
corev1 "k8s.io/api/core/v1"
// +kubebuilder:scaffold:imports
)

Expand Down Expand Up @@ -148,6 +150,21 @@ func main() {
setupLog.Error(err, "unable to create controller", "controller", "QuayRegistryStatus")
os.Exit(1)
}

if err := mgr.GetFieldIndexer().IndexField(context.Background(), &corev1.Event{}, "involvedObject.uid", func(rawObj client.Object) []string {
event, ok := rawObj.(*corev1.Event)
if !ok {
return nil
}
if event.InvolvedObject.UID == "" {
return nil
}
return []string{string(event.InvolvedObject.UID)}
}); err != nil {
setupLog.Error(err, "unable to set up field indexer for Event involvedObject.uid")
os.Exit(1)
}

// +kubebuilder:scaffold:builder

setupLog.Info("starting manager")
Expand Down
70 changes: 12 additions & 58 deletions pkg/cmpstatus/clairpostgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,17 @@ package cmpstatus

import (
"context"
"fmt"
"time"

appsv1 "k8s.io/api/apps/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"

qv1 "github.com/quay/quay-operator/apis/quay/v1"
)

// ClairPostgres checks a quay registry clairpostgres component status. In order to evaluate the status for the
// clair component we need to verify if clairpostgres succeed.
// ClairPostgres checks a quay registry clairpostgres component status.
type ClairPostgres struct {
Client client.Client
deploy deploy
}

// Name returns the component name this entity checks for health.
Expand All @@ -27,11 +21,9 @@ func (c *ClairPostgres) Name() string {
}

// Check verifies if the clairpostgres deployment associated with provided quay registry
// were created and rolled out as expected.
func (c *ClairPostgres) Check(ctx context.Context, reg qv1.QuayRegistry) (qv1.Condition, error) {
var zero qv1.Condition

if !qv1.ComponentIsManaged(reg.Spec.Components, qv1.ComponentClairPostgres) {
// was created and rolled out as expected, also checking its PVC status dynamically.
func (c *ClairPostgres) Check(ctx context.Context, quay qv1.QuayRegistry) (qv1.Condition, error) {
if !qv1.ComponentIsManaged(quay.Spec.Components, qv1.ComponentClairPostgres) {
return qv1.Condition{
Type: qv1.ComponentClairPostgresReady,
Status: metav1.ConditionTrue,
Expand All @@ -41,50 +33,12 @@ func (c *ClairPostgres) Check(ctx context.Context, reg qv1.QuayRegistry) (qv1.Co
}, nil
}

depname := fmt.Sprintf("%s-%s", reg.Name, "clair-postgres")
nsn := types.NamespacedName{
Namespace: reg.Namespace,
Name: depname,
}

var dep appsv1.Deployment
if err := c.Client.Get(ctx, nsn, &dep); err != nil {
if errors.IsNotFound(err) {
msg := fmt.Sprintf("Deployment %s not found", depname)
return qv1.Condition{
Type: qv1.ComponentClairPostgresReady,
Status: metav1.ConditionFalse,
Reason: qv1.ConditionReasonComponentNotReady,
Message: msg,
LastUpdateTime: metav1.NewTime(time.Now()),
}, nil
}
return zero, err
}

if !qv1.Owns(reg, &dep) {
msg := fmt.Sprintf("Deployment %s not owned by QuayRegistry", depname)
return qv1.Condition{
Type: qv1.ComponentClairPostgresReady,
Status: metav1.ConditionFalse,
Reason: qv1.ConditionReasonComponentNotReady,
Message: msg,
LastUpdateTime: metav1.NewTime(time.Now()),
}, nil
}

cond := c.deploy.check(dep)
if cond.Status != metav1.ConditionTrue {
// if the deployment is in a faulty state bails out immediately.
cond.Type = qv1.ComponentClairPostgresReady
return cond, nil
}

return qv1.Condition{
Type: qv1.ComponentClairPostgresReady,
Reason: qv1.ConditionReasonComponentReady,
Status: metav1.ConditionTrue,
Message: "ClairPostgres component healthy",
LastUpdateTime: metav1.NewTime(time.Now()),
}, nil
return CheckDatabaseDeploymentAndPVCStatus(
ctx,
c.Client,
quay,
qv1.ComponentClairPostgres,
"clair-postgres",
qv1.ComponentClairPostgresReady,
)
}
Loading
Loading