diff --git a/apis/quay/v1/quayregistry_types.go b/apis/quay/v1/quayregistry_types.go index 4243af7d9..9245fcb75 100644 --- a/apis/quay/v1/quayregistry_types.go +++ b/apis/quay/v1/quayregistry_types.go @@ -86,9 +86,14 @@ var supportsVolumeOverride = []ComponentKind{ ComponentClairPostgres, } +var supportsEphemeralVolumeOverride = []ComponentKind{ + ComponentClair, +} + var supportsStorageClassOverride = []ComponentKind{ ComponentPostgres, ComponentClairPostgres, + ComponentClair, } var supportsEnvOverride = []ComponentKind{ @@ -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. @@ -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) @@ -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", @@ -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": @@ -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) { diff --git a/apis/quay/v1/quayregistry_types_test.go b/apis/quay/v1/quayregistry_types_test.go index 105c63524..ff668b6c9 100644 --- a/apis/quay/v1/quayregistry_types_test.go +++ b/apis/quay/v1/quayregistry_types_test.go @@ -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}, @@ -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) { diff --git a/apis/quay/v1/zz_generated.deepcopy.go b/apis/quay/v1/zz_generated.deepcopy.go index 9a2756f8d..17def472e 100644 --- a/apis/quay/v1/zz_generated.deepcopy.go +++ b/apis/quay/v1/zz_generated.deepcopy.go @@ -111,6 +111,11 @@ func (in *Override) DeepCopyInto(out *Override) { *out = new(Resources) (*in).DeepCopyInto(*out) } + if in.UseEphemeralVolume != nil { + in, out := &in.UseEphemeralVolume, &out.UseEphemeralVolume + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Override. diff --git a/bundle/manifests/quayregistries.crd.yaml b/bundle/manifests/quayregistries.crd.yaml index 429eba310..4ca634fee 100644 --- a/bundle/manifests/quayregistries.crd.yaml +++ b/bundle/manifests/quayregistries.crd.yaml @@ -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 diff --git a/config/crd/bases/quay.redhat.com_quayregistries.yaml b/config/crd/bases/quay.redhat.com_quayregistries.yaml index 429eba310..4ca634fee 100644 --- a/config/crd/bases/quay.redhat.com_quayregistries.yaml +++ b/config/crd/bases/quay.redhat.com_quayregistries.yaml @@ -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 diff --git a/e2e/ephemeralvolume_overrides/00-assert.yaml b/e2e/ephemeralvolume_overrides/00-assert.yaml new file mode 100644 index 000000000..06fc8969b --- /dev/null +++ b/e2e/ephemeralvolume_overrides/00-assert.yaml @@ -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" + ) \ No newline at end of file diff --git a/e2e/ephemeralvolume_overrides/00-create-quay-registry.yaml b/e2e/ephemeralvolume_overrides/00-create-quay-registry.yaml new file mode 100644 index 000000000..05238b0e5 --- /dev/null +++ b/e2e/ephemeralvolume_overrides/00-create-quay-registry.yaml @@ -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 \ No newline at end of file diff --git a/e2e/storageclass_overrides/00-create-quay-registry.yaml b/e2e/storageclass_overrides/00-create-quay-registry.yaml index fc14f0ab3..d1aeb54e7 100644 --- a/e2e/storageclass_overrides/00-create-quay-registry.yaml +++ b/e2e/storageclass_overrides/00-create-quay-registry.yaml @@ -3,7 +3,6 @@ kind: QuayRegistry metadata: name: skynet spec: - configBundleSecret: config-bundle-secret components: - kind: postgres managed: true diff --git a/pkg/cmpstatus/clair.go b/pkg/cmpstatus/clair.go index fc4c9276d..3d4181391 100644 --- a/pkg/cmpstatus/clair.go +++ b/pkg/cmpstatus/clair.go @@ -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" @@ -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(®, 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. diff --git a/pkg/cmpstatus/clair_test.go b/pkg/cmpstatus/clair_test.go index ac34732e2..27ad775e1 100644 --- a/pkg/cmpstatus/clair_test.go +++ b/pkg/cmpstatus/clair_test.go @@ -9,6 +9,8 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" @@ -329,12 +331,187 @@ func TestClairCheck(t *testing.T) { Message: "Clair manually scaled down", }, }, + { + name: "clair ephemeral volume PVC pending", + quay: qv1.QuayRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: "registry", + UID: "uid", + }, + Spec: qv1.QuayRegistrySpec{ + Components: []qv1.Component{ + { + Kind: qv1.ComponentClair, + Managed: true, + Overrides: &qv1.Override{ + UseEphemeralVolume: ptr.To(true), + }, + }, + }, + }, + }, + objs: []client.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "registry-clair-app", + OwnerReferences: []metav1.OwnerReference{{ + Kind: "QuayRegistry", + Name: "registry", + APIVersion: "quay.redhat.com/v1", + UID: "uid", + }}, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{{ + Name: "clair-tmp", + VolumeSource: corev1.VolumeSource{ + Ephemeral: &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{}, + }, + }, + }}, + }, + }, + }, + }, + &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rs1", + Namespace: "", + OwnerReferences: []metav1.OwnerReference{{Kind: "Deployment", Name: "registry-clair-app"}}, + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + OwnerReferences: []metav1.OwnerReference{{Kind: "ReplicaSet", Name: "rs1"}}, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc1", + OwnerReferences: []metav1.OwnerReference{{Kind: "Pod", Name: "pod1"}}, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + }, + }, + cond: qv1.Condition{ + Type: qv1.ComponentClairReady, + Status: metav1.ConditionFalse, + Reason: qv1.ConditionReasonPVCPending, + Message: "Clair /tmp PersistentVolumeClaim pvc1 is pending", + }, + }, + { + name: "clair ephemeral volume PVC provisioning failed", + quay: qv1.QuayRegistry{ + ObjectMeta: metav1.ObjectMeta{ + Name: "registry", + UID: "uid", + }, + Spec: qv1.QuayRegistrySpec{ + Components: []qv1.Component{ + { + Kind: qv1.ComponentClair, + Managed: true, + Overrides: &qv1.Override{ + UseEphemeralVolume: ptr.To(true), + }, + }, + }, + }, + }, + objs: []client.Object{ + &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: "registry-clair-app", + OwnerReferences: []metav1.OwnerReference{{ + Kind: "QuayRegistry", + Name: "registry", + APIVersion: "quay.redhat.com/v1", + UID: "uid", + }}, + }, + Spec: appsv1.DeploymentSpec{ + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Volumes: []corev1.Volume{{ + Name: "clair-tmp", + VolumeSource: corev1.VolumeSource{ + Ephemeral: &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{}, + }, + }, + }}, + }, + }, + }, + }, + &appsv1.ReplicaSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "rs1", + Namespace: "", + OwnerReferences: []metav1.OwnerReference{{Kind: "Deployment", Name: "registry-clair-app"}}, + }, + }, + &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pod1", + OwnerReferences: []metav1.OwnerReference{{Kind: "ReplicaSet", Name: "rs1"}}, + }, + }, + &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "pvc1", + UID: "pvcuid", + OwnerReferences: []metav1.OwnerReference{{Kind: "Pod", Name: "pod1"}}, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Phase: corev1.ClaimPending, + }, + }, + &corev1.Event{ + ObjectMeta: metav1.ObjectMeta{ + Name: "event1", + Namespace: "", + }, + InvolvedObject: corev1.ObjectReference{ + UID: "pvcuid", + }, + Reason: "ProvisioningFailed", + Message: "provisioning failed for some reason", + }, + }, + cond: qv1.Condition{ + Type: qv1.ComponentClairReady, + Status: metav1.ConditionFalse, + Reason: qv1.ConditionReasonPVCProvisioningFailed, + Message: "Clair /tmp PersistentVolumeClaim provisioning failed: provisioning failed for some reason", + }, + }, } { t.Run(tt.name, func(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - cli := fake.NewClientBuilder().WithObjects(tt.objs...).Build() + scheme := runtime.NewScheme() + if err := qv1.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add quay/v1 to scheme: %s", err) + } + if err := clientgoscheme.AddToScheme(scheme); err != nil { + t.Fatalf("failed to add client-go to scheme: %s", err) + } + + cliBuilder := fake.NewClientBuilder().WithScheme(scheme).WithObjects(tt.objs...) + cliBuilder.WithIndex(&corev1.Event{}, "involvedObject.uid", func(rawObj client.Object) []string { + event := rawObj.(*corev1.Event) + return []string{string(event.InvolvedObject.UID)} + }) + cli := cliBuilder.Build() clair := Clair{ Client: cli, } diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index e49adea83..9328eba65 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -21,8 +21,11 @@ import ( ) const ( - configSecretPrefix = "quay-config-secret" - fieldGroupsAnnotation = "quay-managed-fieldgroups" + configSecretPrefix = "quay-config-secret" + fieldGroupsAnnotation = "quay-managed-fieldgroups" + clairAppDeploymentSuffix = "clair-app" + quayAppDeploymentSuffix = "quay-app" + quayMirrorDeploymentSuffix = "quay-mirror" ) // Process applies any additional middleware steps to a managed k8s object that cannot be @@ -121,9 +124,9 @@ func Process(quay *v1.QuayRegistry, qctx *quaycontext.QuayRegistryContext, obj c // controller. if !v1.ComponentIsManaged(quay.Spec.Components, v1.ComponentHPA) { for kind, depsuffix := range map[v1.ComponentKind]string{ - v1.ComponentClair: "clair-app", - v1.ComponentMirror: "quay-mirror", - v1.ComponentQuay: "quay-app", + v1.ComponentClair: clairAppDeploymentSuffix, + v1.ComponentMirror: quayMirrorDeploymentSuffix, + v1.ComponentQuay: quayAppDeploymentSuffix, } { if !strings.HasSuffix(dep.Name, depsuffix) { continue @@ -211,6 +214,36 @@ func Process(quay *v1.QuayRegistry, qctx *quaycontext.QuayRegistryContext, obj c } dep.Spec.Template.Annotations[fieldGroupsAnnotation] = strings.Join(fgns, ",") + + // --- CLAIR EPHEMERAL VOLUME INJECTION --- + if isClairAppDeployment(dep.Name, kind) && v1.ComponentIsManaged(quay.Spec.Components, v1.ComponentClair) { + useEphemeral := false + var storageClassName *string + var volumeSize resource.Quantity + volumeSizeSet := false + for _, cmp := range quay.Spec.Components { + if cmp.Kind == v1.ComponentClair && cmp.Overrides != nil { + if cmp.Overrides.UseEphemeralVolume != nil { + useEphemeral = *cmp.Overrides.UseEphemeralVolume + } + if cmp.Overrides.StorageClassName != nil { + storageClassName = cmp.Overrides.StorageClassName + } + if cmp.Overrides.VolumeSize != nil { + volumeSize = *cmp.Overrides.VolumeSize + volumeSizeSet = true + } + } + } + if useEphemeral { + // Default to 10Gi if not set + if !volumeSizeSet { + volumeSize = resource.MustParse("10Gi") + } + injectClairEphemeralVolume(dep, storageClassName, volumeSize) + } + } + return dep, nil } @@ -363,3 +396,62 @@ func FlattenSecret(configBundle *corev1.Secret) (*corev1.Secret, error) { flattenedSecret.Data["config.yaml"] = flattenedConfigYAML return flattenedSecret, nil } + +func isClairAppDeployment(depName string, kind v1.ComponentKind) bool { + return kind == v1.ComponentClair && strings.HasSuffix(depName, clairAppDeploymentSuffix) +} + +// injectClairEphemeralVolume adds an ephemeral volume for /tmp to the Clair deployment +func injectClairEphemeralVolume(dep *appsv1.Deployment, storageClassName *string, volumeSize resource.Quantity) { + // Check if the volume already exists (idempotency) + found := false + for _, v := range dep.Spec.Template.Spec.Volumes { + if v.Name == "clair-tmp" { + found = true + break + } + } + if !found { + // Inject the ephemeral volume + vol := corev1.Volume{ + Name: "clair-tmp", + VolumeSource: corev1.VolumeSource{ + Ephemeral: &corev1.EphemeralVolumeSource{ + VolumeClaimTemplate: &corev1.PersistentVolumeClaimTemplate{ + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: volumeSize, + }, + }, + }, + }, + }, + }, + } + if storageClassName != nil { + vol.VolumeSource.Ephemeral.VolumeClaimTemplate.Spec.StorageClassName = storageClassName + } + dep.Spec.Template.Spec.Volumes = append(dep.Spec.Template.Spec.Volumes, vol) + } + + // Inject the volumeMount for /tmp if not already present + for i, c := range dep.Spec.Template.Spec.Containers { + if c.Name == clairAppDeploymentSuffix { + foundMount := false + for _, m := range c.VolumeMounts { + if m.Name == "clair-tmp" && m.MountPath == "/tmp" { + foundMount = true + break + } + } + if !foundMount { + dep.Spec.Template.Spec.Containers[i].VolumeMounts = append(dep.Spec.Template.Spec.Containers[i].VolumeMounts, corev1.VolumeMount{ + Name: "clair-tmp", + MountPath: "/tmp", + }) + } + } + } +}