Skip to content

Commit 8666d5f

Browse files
committed
feat: Add DeploymentConfig support to registry+v1 bundle renderer
This PR implements **Phase 2** of the [Deployment Configuration RFC](https://docs.google.com/document/d/18O4qBvu5I4WIJgo5KU1opyUKcrfgk64xsI3tyXxmVEU/edit?tab=t.0): extending the OLMv1 bundle renderer to apply `DeploymentConfig` customizations to operator deployments. Building on the foundation from #2454, this PR enables the renderer to accept and apply deployment configuration when rendering registry+v1 bundles. The implementation follows OLMv0's behavior patterns to ensure compatibility and correctness. The next PR will wire up the config in the `ClusterExtension` controller by parsing `spec.install.config` to convert to `DeploymentConfig` and thread `DeploymentConfig` through the controller's render call chain
1 parent fbe909f commit 8666d5f

5 files changed

Lines changed: 932 additions & 0 deletions

File tree

internal/operator-controller/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ import (
3232

3333
"github.com/santhosh-tekuri/jsonschema/v6"
3434
"sigs.k8s.io/yaml"
35+
36+
"github.com/operator-framework/api/pkg/operators/v1alpha1"
3537
)
3638

3739
const (
@@ -47,6 +49,10 @@ const (
4749
FormatSingleNamespaceInstallMode = "singleNamespaceInstallMode"
4850
)
4951

52+
// DeploymentConfig is a type alias for v1alpha1.SubscriptionConfig
53+
// to maintain clear naming in the OLMv1 context while reusing the v0 type.
54+
type DeploymentConfig = v1alpha1.SubscriptionConfig
55+
5056
// SchemaProvider lets each package format type describe what configuration it accepts.
5157
//
5258
// Different package format types provide schemas in different ways:

internal/operator-controller/rukpak/render/registryv1/generators/generators.go

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"github.com/operator-framework/api/pkg/operators/v1alpha1"
2222
registrybundle "github.com/operator-framework/operator-registry/pkg/lib/bundle"
2323

24+
"github.com/operator-framework/operator-controller/internal/operator-controller/config"
2425
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/bundle"
2526
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/render"
2627
"github.com/operator-framework/operator-controller/internal/operator-controller/rukpak/util"
@@ -98,6 +99,9 @@ func BundleCSVDeploymentGenerator(rv1 *bundle.RegistryV1, opts render.Options) (
9899
ensureCorrectDeploymentCertVolumes(deploymentResource, *secretInfo)
99100
}
100101

102+
// Apply deployment configuration if provided
103+
applyCustomConfigToDeployment(deploymentResource, opts.DeploymentConfig)
104+
101105
objs = append(objs, deploymentResource)
102106
}
103107
return objs, nil
@@ -578,3 +582,251 @@ func getWebhookNamespaceSelector(targetNamespaces []string) *metav1.LabelSelecto
578582
}
579583
return nil
580584
}
585+
586+
// applyCustomConfigToDeployment applies the deployment configuration to all containers in the deployment.
587+
// It follows OLMv0 behavior for applying configuration to deployments.
588+
// See https://github.com/operator-framework/operator-lifecycle-manager/blob/master/pkg/controller/operators/catalog/subscription/config_types.go
589+
func applyCustomConfigToDeployment(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
590+
if config == nil {
591+
return
592+
}
593+
594+
// Apply all configuration modifications following OLMv0 behavior
595+
applyEnvironmentConfig(deployment, config)
596+
applyEnvironmentFromConfig(deployment, config)
597+
applyVolumeConfig(deployment, config)
598+
applyVolumeMountConfig(deployment, config)
599+
applyTolerationsConfig(deployment, config)
600+
applyResourcesConfig(deployment, config)
601+
applyNodeSelectorConfig(deployment, config)
602+
applyAffinityConfig(deployment, config)
603+
applyAnnotationsConfig(deployment, config)
604+
}
605+
606+
// applyEnvironmentConfig applies environment variables to all containers in the deployment.
607+
// Environment variables from config override existing environment variables with the same name.
608+
// This follows OLMv0 behavior:
609+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L11-L27
610+
func applyEnvironmentConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
611+
if len(config.Env) == 0 {
612+
return
613+
}
614+
615+
for i := range deployment.Spec.Template.Spec.Containers {
616+
container := &deployment.Spec.Template.Spec.Containers[i]
617+
618+
// Create a map to track existing env var names for override behavior
619+
existingEnvMap := make(map[string]int)
620+
for idx, env := range container.Env {
621+
existingEnvMap[env.Name] = idx
622+
}
623+
624+
// Apply config env vars, overriding existing ones with same name
625+
for _, configEnv := range config.Env {
626+
if existingIdx, exists := existingEnvMap[configEnv.Name]; exists {
627+
// Override existing env var
628+
container.Env[existingIdx] = configEnv
629+
} else {
630+
// Append new env var
631+
container.Env = append(container.Env, configEnv)
632+
}
633+
}
634+
}
635+
}
636+
637+
// applyEnvironmentFromConfig appends EnvFrom sources to all containers in the deployment.
638+
// Duplicate EnvFrom sources are not added.
639+
// This follows OLMv0 behavior:
640+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L65-L81
641+
func applyEnvironmentFromConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
642+
if len(config.EnvFrom) == 0 {
643+
return
644+
}
645+
646+
for i := range deployment.Spec.Template.Spec.Containers {
647+
container := &deployment.Spec.Template.Spec.Containers[i]
648+
649+
// Check for duplicates before appending
650+
for _, configEnvFrom := range config.EnvFrom {
651+
isDuplicate := false
652+
for _, existingEnvFrom := range container.EnvFrom {
653+
if envFromEquals(existingEnvFrom, configEnvFrom) {
654+
isDuplicate = true
655+
break
656+
}
657+
}
658+
if !isDuplicate {
659+
container.EnvFrom = append(container.EnvFrom, configEnvFrom)
660+
}
661+
}
662+
}
663+
}
664+
665+
// envFromEquals checks if two EnvFromSource objects are equal.
666+
func envFromEquals(a, b corev1.EnvFromSource) bool {
667+
if a.Prefix != b.Prefix {
668+
return false
669+
}
670+
671+
if (a.ConfigMapRef == nil) != (b.ConfigMapRef == nil) {
672+
return false
673+
}
674+
if a.ConfigMapRef != nil && b.ConfigMapRef != nil {
675+
if a.ConfigMapRef.Name != b.ConfigMapRef.Name {
676+
return false
677+
}
678+
}
679+
680+
if (a.SecretRef == nil) != (b.SecretRef == nil) {
681+
return false
682+
}
683+
if a.SecretRef != nil && b.SecretRef != nil {
684+
if a.SecretRef.Name != b.SecretRef.Name {
685+
return false
686+
}
687+
}
688+
689+
return true
690+
}
691+
692+
// applyVolumeConfig appends volumes to the deployment's pod spec.
693+
// This follows OLMv0 behavior:
694+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L104-L117
695+
func applyVolumeConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
696+
if len(config.Volumes) == 0 {
697+
return
698+
}
699+
700+
deployment.Spec.Template.Spec.Volumes = append(deployment.Spec.Template.Spec.Volumes, config.Volumes...)
701+
}
702+
703+
// applyVolumeMountConfig appends volume mounts to all containers in the deployment.
704+
// This follows OLMv0 behavior:
705+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L149-L165
706+
func applyVolumeMountConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
707+
if len(config.VolumeMounts) == 0 {
708+
return
709+
}
710+
711+
for i := range deployment.Spec.Template.Spec.Containers {
712+
container := &deployment.Spec.Template.Spec.Containers[i]
713+
container.VolumeMounts = append(container.VolumeMounts, config.VolumeMounts...)
714+
}
715+
}
716+
717+
// applyTolerationsConfig appends tolerations to the deployment's pod spec.
718+
// Duplicate tolerations are not added.
719+
// This follows OLMv0 behavior:
720+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L197-L209
721+
func applyTolerationsConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
722+
if len(config.Tolerations) == 0 {
723+
return
724+
}
725+
726+
// Check for duplicates before appending
727+
for _, configToleration := range config.Tolerations {
728+
isDuplicate := false
729+
for _, existingToleration := range deployment.Spec.Template.Spec.Tolerations {
730+
if tolerationEquals(existingToleration, configToleration) {
731+
isDuplicate = true
732+
break
733+
}
734+
}
735+
if !isDuplicate {
736+
deployment.Spec.Template.Spec.Tolerations = append(deployment.Spec.Template.Spec.Tolerations, configToleration)
737+
}
738+
}
739+
}
740+
741+
// tolerationEquals checks if two Toleration objects are equal.
742+
func tolerationEquals(a, b corev1.Toleration) bool {
743+
return a.Key == b.Key &&
744+
a.Operator == b.Operator &&
745+
a.Value == b.Value &&
746+
a.Effect == b.Effect &&
747+
((a.TolerationSeconds == nil && b.TolerationSeconds == nil) ||
748+
(a.TolerationSeconds != nil && b.TolerationSeconds != nil && *a.TolerationSeconds == *b.TolerationSeconds))
749+
}
750+
751+
// applyResourcesConfig applies resource requirements to all containers in the deployment.
752+
// This completely replaces existing resource requirements.
753+
// This follows OLMv0 behavior:
754+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L236-L255
755+
func applyResourcesConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
756+
if config.Resources == nil {
757+
return
758+
}
759+
760+
for i := range deployment.Spec.Template.Spec.Containers {
761+
container := &deployment.Spec.Template.Spec.Containers[i]
762+
container.Resources = *config.Resources
763+
}
764+
}
765+
766+
// applyNodeSelectorConfig applies node selector to the deployment's pod spec.
767+
// This completely replaces existing node selector.
768+
// This follows OLMv0 behavior:
769+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L257-L271
770+
func applyNodeSelectorConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
771+
if config.NodeSelector == nil {
772+
return
773+
}
774+
775+
deployment.Spec.Template.Spec.NodeSelector = config.NodeSelector
776+
}
777+
778+
// applyAffinityConfig applies affinity configuration to the deployment's pod spec.
779+
// This selectively overrides non-nil affinity sub-attributes.
780+
// This follows OLMv0 behavior:
781+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L273-L341
782+
func applyAffinityConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
783+
if config.Affinity == nil {
784+
return
785+
}
786+
787+
if deployment.Spec.Template.Spec.Affinity == nil {
788+
deployment.Spec.Template.Spec.Affinity = &corev1.Affinity{}
789+
}
790+
791+
if config.Affinity.NodeAffinity != nil {
792+
deployment.Spec.Template.Spec.Affinity.NodeAffinity = config.Affinity.NodeAffinity
793+
}
794+
795+
if config.Affinity.PodAffinity != nil {
796+
deployment.Spec.Template.Spec.Affinity.PodAffinity = config.Affinity.PodAffinity
797+
}
798+
799+
if config.Affinity.PodAntiAffinity != nil {
800+
deployment.Spec.Template.Spec.Affinity.PodAntiAffinity = config.Affinity.PodAntiAffinity
801+
}
802+
}
803+
804+
// applyAnnotationsConfig applies annotations to the deployment and its pod template.
805+
// Existing deployment and pod annotations take precedence over config annotations (no override).
806+
// This follows OLMv0 behavior:
807+
// https://github.com/operator-framework/operator-lifecycle-manager/blob/v0.39.0/pkg/controller/operators/olm/overrides/inject/inject.go#L343-L378
808+
func applyAnnotationsConfig(deployment *appsv1.Deployment, config *config.DeploymentConfig) {
809+
if len(config.Annotations) == 0 {
810+
return
811+
}
812+
813+
// Apply to deployment metadata
814+
if deployment.Annotations == nil {
815+
deployment.Annotations = make(map[string]string)
816+
}
817+
for key, value := range config.Annotations {
818+
if _, exists := deployment.Annotations[key]; !exists {
819+
deployment.Annotations[key] = value
820+
}
821+
}
822+
823+
// Apply to pod template metadata
824+
if deployment.Spec.Template.Annotations == nil {
825+
deployment.Spec.Template.Annotations = make(map[string]string)
826+
}
827+
for key, value := range config.Annotations {
828+
if _, exists := deployment.Spec.Template.Annotations[key]; !exists {
829+
deployment.Spec.Template.Annotations[key] = value
830+
}
831+
}
832+
}

0 commit comments

Comments
 (0)