Skip to content
Open
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
59 changes: 53 additions & 6 deletions apis/quay/v1/quayregistry_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,14 @@ var supportsVolumeOverride = []ComponentKind{
ComponentClairPostgres,
}

var supportsEphemeralVolumeOverride = []ComponentKind{
ComponentClair,
}

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

var supportsEnvOverride = []ComponentKind{
Expand Down Expand Up @@ -157,11 +162,12 @@ type Override struct {
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"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Resources *Resources `json:"resources,omitempty"`
Replicas *int32 `json:"replicas,omitempty"`
Affinity *corev1.Affinity `json:"affinity,omitempty"`
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Resources *Resources `json:"resources,omitempty"`
UseEphemeralVolume *bool `json:"useEphemeralVolume,omitempty"`
}

// Resources describes the resource limits and requests for a component.
Expand Down Expand Up @@ -519,10 +525,11 @@ func ValidateOverrides(quay *QuayRegistry) error {
hasaffinity := hasAffinity(component)
hasvolume := component.Overrides.VolumeSize != nil
hasstorageclass := component.Overrides.StorageClassName != nil
hasephemeralvolume := component.Overrides.UseEphemeralVolume != nil
hasreplicas := component.Overrides.Replicas != nil
hasresources := component.Overrides.Resources != nil
hasenvvar := len(component.Overrides.Env) > 0
hasoverride := hasaffinity || hasvolume || hasenvvar || hasreplicas
hasoverride := hasaffinity || hasvolume || hasenvvar || hasreplicas || hasephemeralvolume

if hasoverride && !ComponentIsManaged(quay.Spec.Components, component.Kind) {
return fmt.Errorf("cannot set overrides on unmanaged %s", component.Kind)
Expand Down Expand Up @@ -558,6 +565,34 @@ func ValidateOverrides(quay *QuayRegistry) error {
)
}

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

if (hasstorageclass || hasvolume) && ComponentSupportsOverride(component.Kind, "ephemeralVolume") {
useEphemeral := false
if hasephemeralvolume {
useEphemeral = *component.Overrides.UseEphemeralVolume
}

if !useEphemeral && (component.Overrides.StorageClassName != nil || component.Overrides.VolumeSize != nil) {
return fmt.Errorf("component %s with storageClassName/volumeSize override requires useEphemeralVolume to be set to true",
component.Kind)
}

if useEphemeral && component.Overrides.VolumeSize != nil {
minGi, _ := resource.ParseQuantity("1Gi")
if component.Overrides.VolumeSize.Cmp(minGi) < 0 {
return fmt.Errorf("component %s requires volumeSize to be at least 1Gi when useEphemeralVolume is true", component.Kind)
}
}
}

if hasenvvar && !ComponentSupportsOverride(component.Kind, "env") {
return fmt.Errorf(
"component %s does not support env overrides",
Expand Down Expand Up @@ -753,6 +788,8 @@ func ComponentSupportsOverride(component ComponentKind, override string) bool {
components = supportsVolumeOverride
case "storageClassName":
components = supportsStorageClassOverride
case "ephemeralVolume":
components = supportsEphemeralVolumeOverride
case "env":
components = supportsEnvOverride
case "replicas":
Expand Down Expand Up @@ -917,6 +954,16 @@ func GetAnnotationsOverrideForComponent(quay *QuayRegistry, kind ComponentKind)
return nil
}

// GetUseEphemeralVolumeOverrideForComponent returns the UseEphemeralVolume override for a given component kind.
func GetUseEphemeralVolumeOverrideForComponent(quay *QuayRegistry, kind ComponentKind) *bool {
for _, component := range quay.Spec.Components {
if component.Kind == kind && component.Overrides != nil && component.Overrides.UseEphemeralVolume != nil {
return component.Overrides.UseEphemeralVolume
}
}
return nil
}

// RemoveUnusedConditions is used to trim off conditions created by previous releases of this
// operator that are not used anymore.
func RemoveUnusedConditions(quay *QuayRegistry) {
Expand Down
42 changes: 41 additions & 1 deletion apis/quay/v1/quayregistry_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -453,7 +453,7 @@ var validateOverridesTests = []struct {
{Kind: "clair", Managed: true},
{Kind: "objectstorage", Managed: true},
{Kind: "route", Managed: true},
{Kind: "tls", Managed: true, Overrides: &Override{Env: []corev1.EnvVar{corev1.EnvVar{Name: "foo", Value: "bar"}}}},
{Kind: "tls", Managed: true, Overrides: &Override{Env: []corev1.EnvVar{{Name: "foo", Value: "bar"}}}},
{Kind: "horizontalpodautoscaler", Managed: true},
{Kind: "mirror", Managed: true},
{Kind: "monitoring", Managed: true},
Expand Down Expand Up @@ -501,6 +501,46 @@ var validateOverridesTests = []struct {
},
errors.New("component redis does not support storageClassName overrides"),
},
{
"InvalidEphemeralVolumeOverride",
QuayRegistry{
Spec: QuayRegistrySpec{
Components: []Component{
{Kind: "postgres", Managed: true},
{Kind: "clairpostgres", Managed: true},
{Kind: "redis", Managed: true, Overrides: &Override{UseEphemeralVolume: ptr.To(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},
},
},
},
errors.New("component redis does not support ephemeralVolume overrides"),
},
{
"InvalidOverridesWithoutEphemeralVolumeOverride",
QuayRegistry{
Spec: QuayRegistrySpec{
Components: []Component{
{Kind: "postgres", Managed: true},
{Kind: "clairpostgres", Managed: true},
{Kind: "redis", Managed: true},
{Kind: "clair", Managed: true, Overrides: &Override{StorageClassName: ptr.To("foo")}},
{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 clair with storageClassName/volumeSize override requires useEphemeralVolume to be set to true"),
},
}

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.

2 changes: 2 additions & 0 deletions bundle/manifests/quayregistries.crd.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,8 @@ spec:
description: StorageClassName is the name of the StorageClass
to use for the PVC.
type: string
useEphemeralVolume:
type: boolean
volumeSize:
anyOf:
- type: integer
Expand Down
2 changes: 2 additions & 0 deletions config/crd/bases/quay.redhat.com_quayregistries.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -984,6 +984,8 @@ spec:
description: StorageClassName is the name of the StorageClass
to use for the PVC.
type: string
useEphemeralVolume:
type: boolean
volumeSize:
anyOf:
- type: integer
Expand Down
30 changes: 30 additions & 0 deletions e2e/ephemeralvolume_overrides/00-assert.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: skynet-clair-app
---
apiVersion: kuttl.dev/v1beta1
kind: TestAssert
resourceRefs:
- apiVersion: apps/v1
kind: Deployment
name: skynet-clair-app
ref: object
assertAll:
- celExpr: |
object.spec.template.spec.containers.exists(c,
c.name == "clair-app" &&
c.volumeMounts.exists(vm,
vm.name == "clair-tmp" &&
vm.mountPath == "/tmp"
)
) &&
object.spec.template.spec.volumes.exists(v,
v.name == "clair-tmp" &&
has(v.ephemeral) &&
has(v.ephemeral.volumeClaimTemplate) &&
has(v.ephemeral.volumeClaimTemplate.spec) &&
"ReadWriteOnce" in v.ephemeral.volumeClaimTemplate.spec.accessModes &&
v.ephemeral.volumeClaimTemplate.spec.storageClassName == "local-path" &&
v.ephemeral.volumeClaimTemplate.spec.resources.requests.storage == "15Gi"
)
12 changes: 12 additions & 0 deletions e2e/ephemeralvolume_overrides/00-create-quay-registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
apiVersion: quay.redhat.com/v1
kind: QuayRegistry
metadata:
name: skynet
spec:
components:
- kind: clair
managed: true
overrides:
useEphemeralVolume: true
volumeSize: 15Gi
storageClassName: local-path
1 change: 0 additions & 1 deletion e2e/storageclass_overrides/00-create-quay-registry.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ kind: QuayRegistry
metadata:
name: skynet
spec:
configBundleSecret: config-bundle-secret
components:
- kind: postgres
managed: true
Expand Down
69 changes: 69 additions & 0 deletions pkg/cmpstatus/clair.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"time"

appsv1 "k8s.io/api/apps/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
Expand Down Expand Up @@ -97,6 +98,74 @@ func (c *Clair) Check(ctx context.Context, reg qv1.QuayRegistry) (qv1.Condition,
}, nil
}

// --- Ephemeral /tmp PVC status check for managed Clair ---
useEphemeralOverride := qv1.GetUseEphemeralVolumeOverrideForComponent(&reg, qv1.ComponentClair)
useEphemeral := useEphemeralOverride != nil && *useEphemeralOverride

if useEphemeral {
// List ReplicaSets owned by the deployment
var rsList appsv1.ReplicaSetList
if err := c.Client.List(ctx, &rsList, client.InNamespace(reg.Namespace)); err == nil {
for _, rs := range rsList.Items {
for _, owner := range rs.OwnerReferences {
if owner.Kind == "Deployment" && owner.Name == dep.Name {
// List Pods owned by this ReplicaSet
var podList v1.PodList
if err := c.Client.List(ctx, &podList, client.InNamespace(reg.Namespace)); err == nil {
for _, pod := range podList.Items {
for _, podOwner := range pod.OwnerReferences {
if podOwner.Kind == "ReplicaSet" && podOwner.Name == rs.Name {
// List PVCs owned by this Pod
var pvcList v1.PersistentVolumeClaimList
if err := c.Client.List(ctx, &pvcList, client.InNamespace(reg.Namespace)); err == nil {
for _, pvc := range pvcList.Items {
for _, pvcOwner := range pvc.OwnerReferences {
if pvcOwner.Kind == "Pod" && pvcOwner.Name == pod.Name {
// Check if this PVC is for the /tmp ephemeral volume (by convention, only one per pod for this feature)
if pvc.Status.Phase == v1.ClaimPending {
// Check for provisioning failure events
var eventList v1.EventList
opts := []client.ListOption{
client.InNamespace(reg.Namespace),
client.MatchingFields{"involvedObject.uid": string(pvc.UID)},
}
if err := c.Client.List(ctx, &eventList, opts...); err == nil {
for _, event := range eventList.Items {
if event.Reason == "ProvisioningFailed" {
// Surface provisioning failure in status
return qv1.Condition{
Type: qv1.ComponentClairReady,
Status: metav1.ConditionFalse,
Reason: qv1.ConditionReasonPVCProvisioningFailed,
Message: fmt.Sprintf("Clair /tmp PersistentVolumeClaim provisioning failed: %s", event.Message),
LastUpdateTime: metav1.NewTime(time.Now()),
}, nil
}
}
}
// If pending but no failure event, surface pending status
return qv1.Condition{
Type: qv1.ComponentClairReady,
Status: metav1.ConditionFalse,
Reason: qv1.ConditionReasonPVCPending,
Message: fmt.Sprintf("Clair /tmp PersistentVolumeClaim %s is pending", pvc.Name),
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.
Expand Down
Loading
Loading