diff --git a/cmd/reloader/main.go b/cmd/reloader/main.go index f89a3375..da23618c 100644 --- a/cmd/reloader/main.go +++ b/cmd/reloader/main.go @@ -14,12 +14,14 @@ import ( "github.com/go-logr/zerologr" "github.com/rs/zerolog" "github.com/spf13/cobra" + "k8s.io/client-go/discovery" controllerruntime "sigs.k8s.io/controller-runtime" "github.com/stakater/Reloader/internal/pkg/config" "github.com/stakater/Reloader/internal/pkg/controller" "github.com/stakater/Reloader/internal/pkg/metadata" "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/openshift" ) // Environment variable names for pod identity in HA mode. @@ -115,6 +117,16 @@ func run(cmd *cobra.Command, args []string) error { return fmt.Errorf("creating manager: %w", err) } + if config.ShouldAutoDetectOpenShift() { + restConfig := controllerruntime.GetConfigOrDie() + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restConfig) + if err != nil { + log.V(1).Info("Failed to create discovery client for DeploymentConfig detection", "error", err) + } else if openshift.HasDeploymentConfigSupport(discoveryClient, log) { + cfg.DeploymentConfigEnabled = true + } + } + if err := controller.SetupReconcilers(mgr, cfg, log, &collectors); err != nil { return fmt.Errorf("setting up reconcilers: %w", err) } diff --git a/go.mod b/go.mod index 39587a78..ece1cd8a 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,14 @@ require ( github.com/argoproj/argo-rollouts v1.8.3 github.com/go-logr/logr v1.4.3 github.com/go-logr/zerologr v1.2.3 + github.com/openshift/api v0.0.0-20251223163548-3f584b29ee4a + github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_model v0.6.2 github.com/rs/zerolog v1.34.0 github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 + github.com/spf13/viper v1.21.0 k8s.io/api v0.35.0 k8s.io/apimachinery v0.35.0 k8s.io/client-go v0.35.0 @@ -47,7 +50,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/kr/text v0.2.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -57,11 +59,10 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/common v0.67.4 // indirect github.com/prometheus/procfs v0.19.2 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect + github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect github.com/spf13/afero v1.15.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/viper v1.21.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect diff --git a/go.sum b/go.sum index f7e22e63..caceb32c 100644 --- a/go.sum +++ b/go.sum @@ -8,7 +8,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -19,6 +18,8 @@ github.com/evanphx/json-patch v5.6.0+incompatible h1:jBYDEEiFBPxA0v50tFdvOzQQTCv github.com/evanphx/json-patch v5.6.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.11 h1:/8HVnzMq13/3x9TPvjG08wUGqBTmZBsCWzjTM0wiaDU= github.com/evanphx/json-patch/v5 v5.9.11/go.mod h1:3j+LviiESTElxA4p3EMKAB9HXj3/XEtnUf6OZxqIQTM= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= @@ -116,6 +117,12 @@ github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/openshift/api v0.0.0-20251222154221-d4b2fef98af2 h1:q7fp/9fnJuXWCzpBzbHZne6aMLGYPKhzPy1uULfaJqA= +github.com/openshift/api v0.0.0-20251222154221-d4b2fef98af2/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/api v0.0.0-20251223163548-3f584b29ee4a h1:lz22938uOBlzTHjGpobGeVWkcxGu6fDQ7oZWheClTHE= +github.com/openshift/api v0.0.0-20251223163548-3f584b29ee4a/go.mod h1:d5uzF0YN2nQQFA0jIEWzzOZ+edmo6wzlGLvx5Fhz4uY= +github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc h1:nIlRaJfr/yGjPV15MNF5eVHLAGyXFjcUzO+hXeWDDk8= +github.com/openshift/client-go v0.0.0-20251223102348-558b0eef16bc/go.mod h1:cs9BwTu96sm2vQvy7r9rOiltgu90M6ju2qIHFG9WU+o= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -138,6 +145,8 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= +github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4= +github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= diff --git a/internal/pkg/config/config.go b/internal/pkg/config/config.go index 583b4dca..ead11abc 100644 --- a/internal/pkg/config/config.go +++ b/internal/pkg/config/config.go @@ -26,16 +26,17 @@ const ( // Config holds all configuration for Reloader. type Config struct { - Annotations AnnotationConfig `json:"annotations"` - AutoReloadAll bool `json:"autoReloadAll"` - ReloadStrategy ReloadStrategy `json:"reloadStrategy"` - ArgoRolloutsEnabled bool `json:"argoRolloutsEnabled"` - ArgoRolloutStrategy ArgoRolloutStrategy `json:"argoRolloutStrategy"` - ReloadOnCreate bool `json:"reloadOnCreate"` - ReloadOnDelete bool `json:"reloadOnDelete"` - SyncAfterRestart bool `json:"syncAfterRestart"` - EnableHA bool `json:"enableHA"` - WebhookURL string `json:"webhookUrl,omitempty"` + Annotations AnnotationConfig `json:"annotations"` + AutoReloadAll bool `json:"autoReloadAll"` + ReloadStrategy ReloadStrategy `json:"reloadStrategy"` + ArgoRolloutsEnabled bool `json:"argoRolloutsEnabled"` + ArgoRolloutStrategy ArgoRolloutStrategy `json:"argoRolloutStrategy"` + DeploymentConfigEnabled bool `json:"deploymentConfigEnabled"` + ReloadOnCreate bool `json:"reloadOnCreate"` + ReloadOnDelete bool `json:"reloadOnDelete"` + SyncAfterRestart bool `json:"syncAfterRestart"` + EnableHA bool `json:"enableHA"` + WebhookURL string `json:"webhookUrl,omitempty"` IgnoredResources []string `json:"ignoredResources,omitempty"` IgnoredWorkloads []string `json:"ignoredWorkloads,omitempty"` @@ -101,28 +102,29 @@ type LeaderElectionConfig struct { // NewDefault creates a Config with default values. func NewDefault() *Config { return &Config{ - Annotations: DefaultAnnotations(), - AutoReloadAll: false, - ReloadStrategy: ReloadStrategyEnvVars, - ArgoRolloutsEnabled: false, - ArgoRolloutStrategy: ArgoRolloutStrategyRollout, - ReloadOnCreate: false, - ReloadOnDelete: false, - SyncAfterRestart: false, - EnableHA: false, - WebhookURL: "", - IgnoredResources: []string{}, - IgnoredWorkloads: []string{}, - IgnoredNamespaces: []string{}, - NamespaceSelectors: []labels.Selector{}, - ResourceSelectors: []labels.Selector{}, - LogFormat: "", - LogLevel: "info", - MetricsAddr: ":9090", - HealthAddr: ":8081", - EnablePProf: false, - PProfAddr: ":6060", - Alerting: AlertingConfig{}, + Annotations: DefaultAnnotations(), + AutoReloadAll: false, + ReloadStrategy: ReloadStrategyEnvVars, + ArgoRolloutsEnabled: false, + ArgoRolloutStrategy: ArgoRolloutStrategyRollout, + DeploymentConfigEnabled: false, + ReloadOnCreate: false, + ReloadOnDelete: false, + SyncAfterRestart: false, + EnableHA: false, + WebhookURL: "", + IgnoredResources: []string{}, + IgnoredWorkloads: []string{}, + IgnoredNamespaces: []string{}, + NamespaceSelectors: []labels.Selector{}, + ResourceSelectors: []labels.Selector{}, + LogFormat: "", + LogLevel: "info", + MetricsAddr: ":9090", + HealthAddr: ":8081", + EnablePProf: false, + PProfAddr: ":6060", + Alerting: AlertingConfig{}, LeaderElection: LeaderElectionConfig{ LockName: "reloader-leader-election", LeaseDuration: 15 * time.Second, diff --git a/internal/pkg/config/flags.go b/internal/pkg/config/flags.go index 5de13cf3..9c2556dd 100644 --- a/internal/pkg/config/flags.go +++ b/internal/pkg/config/flags.go @@ -39,6 +39,12 @@ func BindFlags(fs *pflag.FlagSet, cfg *Config) { "Enable Argo Rollouts support (true/false)", ) + // OpenShift DeploymentConfig + fs.String( + "is-openshift", "", + "Enable OpenShift DeploymentConfig support (true/false/auto). Empty or 'auto' enables auto-detection", + ) + // Event watching fs.String( "reload-on-create", "false", @@ -239,6 +245,14 @@ func ApplyFlags(cfg *Config) error { cfg.ReloadOnCreate = parseBoolString(v.GetString("reload-on-create")) cfg.ReloadOnDelete = parseBoolString(v.GetString("reload-on-delete")) + switch strings.ToLower(strings.TrimSpace(v.GetString("is-openshift"))) { + case "true": + cfg.DeploymentConfigEnabled = true + case "false": + cfg.DeploymentConfigEnabled = false + default: + } + // String flags cfg.ReloadStrategy = ReloadStrategy(v.GetString("reload-strategy")) cfg.WebhookURL = v.GetString("webhook-url") @@ -321,6 +335,13 @@ func parseBoolString(s string) bool { return s == "true" || s == "1" || s == "yes" } +// ShouldAutoDetectOpenShift returns true if OpenShift DeploymentConfig support +// should be auto-detected (i.e., the --is-openshift flag was not explicitly set). +func ShouldAutoDetectOpenShift() bool { + val := strings.ToLower(strings.TrimSpace(v.GetString("is-openshift"))) + return val == "" || val == "auto" +} + // splitAndTrim splits a comma-separated string and trims whitespace. func splitAndTrim(s string) []string { if s == "" { diff --git a/internal/pkg/controller/manager.go b/internal/pkg/controller/manager.go index bc83ca70..07ffcfbf 100644 --- a/internal/pkg/controller/manager.go +++ b/internal/pkg/controller/manager.go @@ -6,6 +6,7 @@ import ( argorolloutsv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" "github.com/go-logr/logr" + openshiftv1 "github.com/openshift/api/apps/v1" "github.com/stakater/Reloader/internal/pkg/alerting" "github.com/stakater/Reloader/internal/pkg/config" "github.com/stakater/Reloader/internal/pkg/events" @@ -27,6 +28,7 @@ var runtimeScheme = runtime.NewScheme() func init() { utilruntime.Must(clientgoscheme.AddToScheme(runtimeScheme)) utilruntime.Must(argorolloutsv1alpha1.AddToScheme(runtimeScheme)) + utilruntime.Must(openshiftv1.AddToScheme(runtimeScheme)) } // ManagerOptions contains options for creating a new Manager. @@ -115,7 +117,10 @@ func NewManagerWithRestConfig(opts ManagerOptions, restConfig *rest.Config) (ctr // SetupReconcilers sets up all reconcilers with the manager. func SetupReconcilers(mgr ctrl.Manager, cfg *config.Config, log logr.Logger, collectors *metrics.Collectors) error { - registry := workload.NewRegistry(cfg.ArgoRolloutsEnabled) + registry := workload.NewRegistry(workload.RegistryOptions{ + ArgoRolloutsEnabled: cfg.ArgoRolloutsEnabled, + DeploymentConfigEnabled: cfg.DeploymentConfigEnabled, + }) reloadService := reload.NewService(cfg) eventRecorder := events.NewRecorder(mgr.GetEventRecorderFor("reloader")) pauseHandler := reload.NewPauseHandler(cfg) diff --git a/internal/pkg/controller/test_helpers_test.go b/internal/pkg/controller/test_helpers_test.go index 778abac7..916696ab 100644 --- a/internal/pkg/controller/test_helpers_test.go +++ b/internal/pkg/controller/test_helpers_test.go @@ -36,7 +36,10 @@ func newConfigMapReconciler(t *testing.T, cfg *config.Config, objects ...runtime Log: testr.New(t), Config: cfg, ReloadService: reload.NewService(cfg), - Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled), + Registry: workload.NewRegistry(workload.RegistryOptions{ + ArgoRolloutsEnabled: cfg.ArgoRolloutsEnabled, + DeploymentConfigEnabled: cfg.DeploymentConfigEnabled, + }), Collectors: &collectors, EventRecorder: events.NewRecorder(nil), WebhookClient: webhook.NewClient("", testr.New(t)), @@ -59,7 +62,10 @@ func newSecretReconciler(t *testing.T, cfg *config.Config, objects ...runtime.Ob Log: testr.New(t), Config: cfg, ReloadService: reload.NewService(cfg), - Registry: workload.NewRegistry(cfg.ArgoRolloutsEnabled), + Registry: workload.NewRegistry(workload.RegistryOptions{ + ArgoRolloutsEnabled: cfg.ArgoRolloutsEnabled, + DeploymentConfigEnabled: cfg.DeploymentConfigEnabled, + }), Collectors: &collectors, EventRecorder: events.NewRecorder(nil), WebhookClient: webhook.NewClient("", testr.New(t)), diff --git a/internal/pkg/openshift/detect.go b/internal/pkg/openshift/detect.go new file mode 100644 index 00000000..403c0d27 --- /dev/null +++ b/internal/pkg/openshift/detect.go @@ -0,0 +1,34 @@ +package openshift + +import ( + "github.com/go-logr/logr" + "k8s.io/client-go/discovery" +) + +const ( + // DeploymentConfigAPIGroup is the API group for DeploymentConfig. + DeploymentConfigAPIGroup = "apps.openshift.io" + // DeploymentConfigAPIVersion is the API version for DeploymentConfig. + DeploymentConfigAPIVersion = "v1" + // DeploymentConfigResource is the resource name for DeploymentConfig. + DeploymentConfigResource = "deploymentconfigs" +) + +// HasDeploymentConfigSupport checks if the cluster supports DeploymentConfig +func HasDeploymentConfigSupport(client discovery.DiscoveryInterface, log logr.Logger) bool { + resources, err := client.ServerResourcesForGroupVersion(DeploymentConfigAPIGroup + "/" + DeploymentConfigAPIVersion) + if err != nil { + log.V(1).Info("DeploymentConfig API not available", "error", err) + return false + } + + for _, r := range resources.APIResources { + if r.Name == DeploymentConfigResource { + log.Info("DeploymentConfig API detected, enabling support") + return true + } + } + + log.V(1).Info("DeploymentConfig resource not found in apps.openshift.io/v1") + return false +} diff --git a/internal/pkg/testutil/fixtures.go b/internal/pkg/testutil/fixtures.go index 1deb85a1..6ba8587a 100644 --- a/internal/pkg/testutil/fixtures.go +++ b/internal/pkg/testutil/fixtures.go @@ -1,6 +1,7 @@ package testutil import ( + openshiftv1 "github.com/openshift/api/apps/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -8,12 +9,69 @@ import ( "k8s.io/apimachinery/pkg/runtime" ) +// NewDeploymentConfig creates a minimal DeploymentConfig for unit testing. +func NewDeploymentConfig(name, namespace string, annotations map[string]string) *openshiftv1.DeploymentConfig { + replicas := int32(1) + return &openshiftv1.DeploymentConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Annotations: annotations, + }, + Spec: openshiftv1.DeploymentConfigSpec{ + Replicas: replicas, + Selector: map[string]string{"app": name}, + Template: &corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{"app": name}, + Annotations: map[string]string{}, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "main", + Image: "nginx", + }, + }, + }, + }, + }, + } +} + +// NewDeploymentConfigWithEnvFrom creates a DeploymentConfig with EnvFrom referencing a ConfigMap or Secret. +func NewDeploymentConfigWithEnvFrom(name, namespace string, configMapName, secretName string) *openshiftv1.DeploymentConfig { + dc := NewDeploymentConfig(name, namespace, nil) + if configMapName != "" { + dc.Spec.Template.Spec.Containers[0].EnvFrom = append( + dc.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + }, + }, + ) + } + if secretName != "" { + dc.Spec.Template.Spec.Containers[0].EnvFrom = append( + dc.Spec.Template.Spec.Containers[0].EnvFrom, + corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + }, + }, + ) + } + return dc +} + // NewScheme creates a scheme with common types for testing. func NewScheme() *runtime.Scheme { scheme := runtime.NewScheme() _ = corev1.AddToScheme(scheme) _ = appsv1.AddToScheme(scheme) _ = batchv1.AddToScheme(scheme) + _ = openshiftv1.AddToScheme(scheme) return scheme } @@ -35,10 +93,12 @@ func NewDeployment(name, namespace string, annotations map[string]string) *appsv Annotations: map[string]string{}, }, Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "main", - Image: "nginx", - }}, + Containers: []corev1.Container{ + { + Name: "main", + Image: "nginx", + }, + }, }, }, }, @@ -74,30 +134,36 @@ func NewDeploymentWithEnvFrom(name, namespace string, configMapName, secretName // NewDeploymentWithVolume creates a Deployment with a volume from ConfigMap or Secret. func NewDeploymentWithVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment { d := NewDeployment(name, namespace, nil) - d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{ - Name: "config", - MountPath: "/etc/config", - }} + d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: "config", + MountPath: "/etc/config", + }, + } if configMapName != "" { - d.Spec.Template.Spec.Volumes = []corev1.Volume{{ - Name: "config", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + d.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + }, }, }, - }} + } } if secretName != "" { - d.Spec.Template.Spec.Volumes = []corev1.Volume{{ - Name: "config", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: secretName, + d.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + }, }, }, - }} + } } return d } @@ -105,33 +171,41 @@ func NewDeploymentWithVolume(name, namespace string, configMapName, secretName s // NewDeploymentWithProjectedVolume creates a Deployment with a projected volume. func NewDeploymentWithProjectedVolume(name, namespace string, configMapName, secretName string) *appsv1.Deployment { d := NewDeployment(name, namespace, nil) - d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{{ - Name: "config", - MountPath: "/etc/config", - }} + d.Spec.Template.Spec.Containers[0].VolumeMounts = []corev1.VolumeMount{ + { + Name: "config", + MountPath: "/etc/config", + }, + } sources := []corev1.VolumeProjection{} if configMapName != "" { - sources = append(sources, corev1.VolumeProjection{ - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + sources = append( + sources, corev1.VolumeProjection{ + ConfigMap: &corev1.ConfigMapProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + }, }, - }) + ) } if secretName != "" { - sources = append(sources, corev1.VolumeProjection{ - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + sources = append( + sources, corev1.VolumeProjection{ + Secret: &corev1.SecretProjection{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + }, }, - }) + ) } - d.Spec.Template.Spec.Volumes = []corev1.Volume{{ - Name: "config", - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{Sources: sources}, + d.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config", + VolumeSource: corev1.VolumeSource{ + Projected: &corev1.ProjectedVolumeSource{Sources: sources}, + }, }, - }} + } return d } @@ -153,10 +227,12 @@ func NewDaemonSet(name, namespace string, annotations map[string]string) *appsv1 Annotations: map[string]string{}, }, Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "main", - Image: "nginx", - }}, + Containers: []corev1.Container{ + { + Name: "main", + Image: "nginx", + }, + }, }, }, }, @@ -181,10 +257,12 @@ func NewStatefulSet(name, namespace string, annotations map[string]string) *apps Annotations: map[string]string{}, }, Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "main", - Image: "nginx", - }}, + Containers: []corev1.Container{ + { + Name: "main", + Image: "nginx", + }, + }, }, }, }, @@ -200,18 +278,30 @@ func NewJob(name, namespace string) *batchv1.Job { }, Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{{ - Name: "main", - Image: "busybox", - }}, + Containers: []corev1.Container{ + { + Name: "main", + Image: "busybox", + }, + }, }, }, }, } } +// NewJobWithAnnotations creates a Job with annotations. +func NewJobWithAnnotations(name, namespace string, annotations map[string]string) *batchv1.Job { + job := NewJob(name, namespace) + job.Annotations = annotations + return job +} + // NewCronJob creates a minimal CronJob for unit testing. func NewCronJob(name, namespace string) *batchv1.CronJob { return &batchv1.CronJob{ @@ -225,12 +315,17 @@ func NewCronJob(name, namespace string) *batchv1.CronJob { JobTemplate: batchv1.JobTemplateSpec{ Spec: batchv1.JobSpec{ Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Annotations: map[string]string{}, + }, Spec: corev1.PodSpec{ RestartPolicy: corev1.RestartPolicyNever, - Containers: []corev1.Container{{ - Name: "main", - Image: "busybox", - }}, + Containers: []corev1.Container{ + { + Name: "main", + Image: "busybox", + }, + }, }, }, }, @@ -239,6 +334,13 @@ func NewCronJob(name, namespace string) *batchv1.CronJob { } } +// NewCronJobWithAnnotations creates a CronJob with annotations. +func NewCronJobWithAnnotations(name, namespace string, annotations map[string]string) *batchv1.CronJob { + cj := NewCronJob(name, namespace) + cj.Annotations = annotations + return cj +} + // NewConfigMap creates a ConfigMap for unit testing. func NewConfigMap(name, namespace string) *corev1.ConfigMap { return &corev1.ConfigMap{ diff --git a/internal/pkg/testutil/testutil.go b/internal/pkg/testutil/testutil.go index 96347e4c..63015534 100644 --- a/internal/pkg/testutil/testutil.go +++ b/internal/pkg/testutil/testutil.go @@ -7,13 +7,15 @@ import ( "fmt" "time" - "github.com/stakater/Reloader/internal/pkg/config" + openshiftv1 "github.com/openshift/api/apps/v1" + openshiftclient "github.com/openshift/client-go/apps/clientset/versioned" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" ) const ( @@ -133,7 +135,9 @@ func DeleteSecret(client kubernetes.Interface, namespace, name string) error { } // CreateDeployment creates a Deployment that references a ConfigMap/Secret. -func CreateDeployment(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.Deployment, error) { +func CreateDeployment(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) ( + *appsv1.Deployment, error, +) { var deployment *appsv1.Deployment if useConfigMap { deployment = NewDeploymentWithEnvFrom(name, namespace, name, "") @@ -154,7 +158,9 @@ func DeleteDeployment(client kubernetes.Interface, namespace, name string) error } // CreateDaemonSet creates a DaemonSet that references a ConfigMap/Secret. -func CreateDaemonSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.DaemonSet, error) { +func CreateDaemonSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) ( + *appsv1.DaemonSet, error, +) { daemonset := NewDaemonSet(name, namespace, annotations) // Override image for integration tests daemonset.Spec.Template.Spec.Containers[0].Image = "busybox:1.36" @@ -187,7 +193,9 @@ func DeleteDaemonSet(client kubernetes.Interface, namespace, name string) error } // CreateStatefulSet creates a StatefulSet that references a ConfigMap/Secret. -func CreateStatefulSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) (*appsv1.StatefulSet, error) { +func CreateStatefulSet(client kubernetes.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) ( + *appsv1.StatefulSet, error, +) { statefulset := NewStatefulSet(name, namespace, annotations) statefulset.Spec.ServiceName = name // Override image for integration tests @@ -266,88 +274,157 @@ func ConvertResourceToSHA(resourceType, namespace, name, data string) string { func WaitForDeploymentAnnotation(client kubernetes.Interface, namespace, name, annotation, expectedValue string, timeout time.Duration) error { ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - return wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { - deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep waiting - } - value, ok := deployment.Spec.Template.Annotations[annotation] - if !ok { - return false, nil // Keep waiting - } - return value == expectedValue, nil - }) + return wait.PollUntilContextTimeout( + ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { + deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep waiting + } + value, ok := deployment.Spec.Template.Annotations[annotation] + if !ok { + return false, nil // Keep waiting + } + return value == expectedValue, nil + }, + ) } -// WaitForDeploymentReloadedAnnotation waits for a deployment to have any reloaded annotation. -func WaitForDeploymentReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) { +// WaitForDeploymentReloadedAnnotation waits for a deployment to have the specified reloaded annotation. +func WaitForDeploymentReloadedAnnotation(client kubernetes.Interface, namespace, name, annotationName string, timeout time.Duration) ( + bool, error, +) { var found bool ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - err := wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { - deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep waiting - } - // Check for the last-reloaded-from annotation in pod template - if deployment.Spec.Template.Annotations != nil { - if _, ok := deployment.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok { - found = true - return true, nil + err := wait.PollUntilContextTimeout( + ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { + deployment, err := client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep waiting } - } - return false, nil - }) + // Check for the last-reloaded-from annotation in pod template + if deployment.Spec.Template.Annotations != nil { + if _, ok := deployment.Spec.Template.Annotations[annotationName]; ok { + found = true + return true, nil + } + } + return false, nil + }, + ) if wait.Interrupted(err) { return found, nil } return found, err } -// WaitForDaemonSetReloadedAnnotation waits for a daemonset to have any reloaded annotation. -func WaitForDaemonSetReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) { +// WaitForDaemonSetReloadedAnnotation waits for a daemonset to have the specified reloaded annotation. +func WaitForDaemonSetReloadedAnnotation(client kubernetes.Interface, namespace, name, annotationName string, timeout time.Duration) ( + bool, error, +) { var found bool ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - err := wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { - daemonset, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep waiting - } - // Check for the last-reloaded-from annotation in pod template - if daemonset.Spec.Template.Annotations != nil { - if _, ok := daemonset.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok { - found = true - return true, nil + err := wait.PollUntilContextTimeout( + ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { + daemonset, err := client.AppsV1().DaemonSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep waiting } - } - return false, nil - }) + // Check for the last-reloaded-from annotation in pod template + if daemonset.Spec.Template.Annotations != nil { + if _, ok := daemonset.Spec.Template.Annotations[annotationName]; ok { + found = true + return true, nil + } + } + return false, nil + }, + ) if wait.Interrupted(err) { return found, nil } return found, err } -// WaitForStatefulSetReloadedAnnotation waits for a statefulset to have any reloaded annotation. -func WaitForStatefulSetReloadedAnnotation(client kubernetes.Interface, namespace, name string, cfg *config.Config, timeout time.Duration) (bool, error) { +// WaitForStatefulSetReloadedAnnotation waits for a statefulset to have the specified reloaded annotation. +func WaitForStatefulSetReloadedAnnotation(client kubernetes.Interface, namespace, name, annotationName string, timeout time.Duration) ( + bool, error, +) { var found bool ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() - err := wait.PollUntilContextTimeout(ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { - statefulset, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) - if err != nil { - return false, nil // Keep waiting - } - // Check for the last-reloaded-from annotation in pod template - if statefulset.Spec.Template.Annotations != nil { - if _, ok := statefulset.Spec.Template.Annotations[cfg.Annotations.LastReloadedFrom]; ok { - found = true - return true, nil + err := wait.PollUntilContextTimeout( + ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { + statefulset, err := client.AppsV1().StatefulSets(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep waiting } - } - return false, nil - }) + // Check for the last-reloaded-from annotation in pod template + if statefulset.Spec.Template.Annotations != nil { + if _, ok := statefulset.Spec.Template.Annotations[annotationName]; ok { + found = true + return true, nil + } + } + return false, nil + }, + ) + if wait.Interrupted(err) { + return found, nil + } + return found, err +} + +// NewOpenshiftClient creates an OpenShift client from the given rest config. +func NewOpenshiftClient(restCfg *rest.Config) (openshiftclient.Interface, error) { + return openshiftclient.NewForConfig(restCfg) +} + +// CreateDeploymentConfig creates a DeploymentConfig that references a ConfigMap/Secret. +func CreateDeploymentConfig(client openshiftclient.Interface, name, namespace string, useConfigMap bool, annotations map[string]string) ( + *openshiftv1.DeploymentConfig, error, +) { + var dc *openshiftv1.DeploymentConfig + if useConfigMap { + dc = NewDeploymentConfigWithEnvFrom(name, namespace, name, "") + } else { + dc = NewDeploymentConfigWithEnvFrom(name, namespace, "", name) + } + dc.Annotations = annotations + dc.Spec.Template.Spec.Containers[0].Image = "busybox:1.36" + dc.Spec.Template.Spec.Containers[0].Command = []string{"sh", "-c", "while true; do sleep 3600; done"} + + return client.AppsV1().DeploymentConfigs(namespace).Create(context.Background(), dc, metav1.CreateOptions{}) +} + +// DeleteDeploymentConfig deletes the DeploymentConfig with the given name. +func DeleteDeploymentConfig(client openshiftclient.Interface, namespace, name string) error { + return client.AppsV1().DeploymentConfigs(namespace).Delete(context.Background(), name, metav1.DeleteOptions{}) +} + +// WaitForDeploymentConfigReloadedAnnotation waits for a DeploymentConfig to have the specified reloaded annotation. +func WaitForDeploymentConfigReloadedAnnotation(client openshiftclient.Interface, namespace, name, annotationName string, timeout time.Duration) ( + bool, error, +) { + var found bool + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + err := wait.PollUntilContextTimeout( + ctx, time.Second, timeout, true, func(ctx context.Context) (bool, error) { + dc, err := client.AppsV1().DeploymentConfigs(namespace).Get(ctx, name, metav1.GetOptions{}) + if err != nil { + return false, nil // Keep waiting + } + if dc.Spec.Template != nil && dc.Spec.Template.Annotations != nil { + if _, ok := dc.Spec.Template.Annotations[annotationName]; ok { + found = true + return true, nil + } + } + return false, nil + }, + ) if wait.Interrupted(err) { return found, nil } diff --git a/internal/pkg/workload/deploymentconfig.go b/internal/pkg/workload/deploymentconfig.go new file mode 100644 index 00000000..14c60469 --- /dev/null +++ b/internal/pkg/workload/deploymentconfig.go @@ -0,0 +1,143 @@ +package workload + +import ( + "context" + + openshiftv1 "github.com/openshift/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// DeploymentConfigWorkload wraps an OpenShift DeploymentConfig. +type DeploymentConfigWorkload struct { + dc *openshiftv1.DeploymentConfig +} + +// NewDeploymentConfigWorkload creates a new DeploymentConfigWorkload. +func NewDeploymentConfigWorkload(dc *openshiftv1.DeploymentConfig) *DeploymentConfigWorkload { + return &DeploymentConfigWorkload{dc: dc} +} + +// Ensure DeploymentConfigWorkload implements WorkloadAccessor. +var _ WorkloadAccessor = (*DeploymentConfigWorkload)(nil) + +func (w *DeploymentConfigWorkload) Kind() Kind { + return KindDeploymentConfig +} + +func (w *DeploymentConfigWorkload) GetObject() client.Object { + return w.dc +} + +func (w *DeploymentConfigWorkload) GetName() string { + return w.dc.Name +} + +func (w *DeploymentConfigWorkload) GetNamespace() string { + return w.dc.Namespace +} + +func (w *DeploymentConfigWorkload) GetAnnotations() map[string]string { + return w.dc.Annotations +} + +func (w *DeploymentConfigWorkload) GetPodTemplateAnnotations() map[string]string { + if w.dc.Spec.Template == nil { + return nil + } + if w.dc.Spec.Template.Annotations == nil { + w.dc.Spec.Template.Annotations = make(map[string]string) + } + return w.dc.Spec.Template.Annotations +} + +func (w *DeploymentConfigWorkload) SetPodTemplateAnnotation(key, value string) { + if w.dc.Spec.Template == nil { + w.dc.Spec.Template = &corev1.PodTemplateSpec{} + } + if w.dc.Spec.Template.Annotations == nil { + w.dc.Spec.Template.Annotations = make(map[string]string) + } + w.dc.Spec.Template.Annotations[key] = value +} + +func (w *DeploymentConfigWorkload) GetContainers() []corev1.Container { + if w.dc.Spec.Template == nil { + return nil + } + return w.dc.Spec.Template.Spec.Containers +} + +func (w *DeploymentConfigWorkload) SetContainers(containers []corev1.Container) { + if w.dc.Spec.Template == nil { + w.dc.Spec.Template = &corev1.PodTemplateSpec{} + } + w.dc.Spec.Template.Spec.Containers = containers +} + +func (w *DeploymentConfigWorkload) GetInitContainers() []corev1.Container { + if w.dc.Spec.Template == nil { + return nil + } + return w.dc.Spec.Template.Spec.InitContainers +} + +func (w *DeploymentConfigWorkload) SetInitContainers(containers []corev1.Container) { + if w.dc.Spec.Template == nil { + w.dc.Spec.Template = &corev1.PodTemplateSpec{} + } + w.dc.Spec.Template.Spec.InitContainers = containers +} + +func (w *DeploymentConfigWorkload) GetVolumes() []corev1.Volume { + if w.dc.Spec.Template == nil { + return nil + } + return w.dc.Spec.Template.Spec.Volumes +} + +func (w *DeploymentConfigWorkload) Update(ctx context.Context, c client.Client) error { + return c.Update(ctx, w.dc) +} + +func (w *DeploymentConfigWorkload) DeepCopy() Workload { + return &DeploymentConfigWorkload{dc: w.dc.DeepCopy()} +} + +func (w *DeploymentConfigWorkload) GetEnvFromSources() []corev1.EnvFromSource { + if w.dc.Spec.Template == nil { + return nil + } + var sources []corev1.EnvFromSource + for _, container := range w.dc.Spec.Template.Spec.Containers { + sources = append(sources, container.EnvFrom...) + } + for _, container := range w.dc.Spec.Template.Spec.InitContainers { + sources = append(sources, container.EnvFrom...) + } + return sources +} + +func (w *DeploymentConfigWorkload) UsesConfigMap(name string) bool { + if w.dc.Spec.Template == nil { + return false + } + return SpecUsesConfigMap(&w.dc.Spec.Template.Spec, name) +} + +func (w *DeploymentConfigWorkload) UsesSecret(name string) bool { + if w.dc.Spec.Template == nil { + return false + } + return SpecUsesSecret(&w.dc.Spec.Template.Spec, name) +} + +func (w *DeploymentConfigWorkload) GetOwnerReferences() []metav1.OwnerReference { + return w.dc.OwnerReferences +} + +// GetDeploymentConfig returns the underlying DeploymentConfig for special handling. +func (w *DeploymentConfigWorkload) GetDeploymentConfig() *openshiftv1.DeploymentConfig { + return w.dc +} diff --git a/internal/pkg/workload/interface.go b/internal/pkg/workload/interface.go index 6f805af4..e1d50a18 100644 --- a/internal/pkg/workload/interface.go +++ b/internal/pkg/workload/interface.go @@ -18,12 +18,13 @@ import ( type Kind string const ( - KindDeployment Kind = "Deployment" - KindDaemonSet Kind = "DaemonSet" - KindStatefulSet Kind = "StatefulSet" - KindArgoRollout Kind = "Rollout" - KindJob Kind = "Job" - KindCronJob Kind = "CronJob" + KindDeployment Kind = "Deployment" + KindDaemonSet Kind = "DaemonSet" + KindStatefulSet Kind = "StatefulSet" + KindArgoRollout Kind = "Rollout" + KindJob Kind = "Job" + KindCronJob Kind = "CronJob" + KindDeploymentConfig Kind = "DeploymentConfig" ) // Workload provides a uniform interface for managing Kubernetes workloads. diff --git a/internal/pkg/workload/lister.go b/internal/pkg/workload/lister.go index a30bdfcc..07cde615 100644 --- a/internal/pkg/workload/lister.go +++ b/internal/pkg/workload/lister.go @@ -4,6 +4,7 @@ import ( "context" argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + openshiftv1 "github.com/openshift/api/apps/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -128,3 +129,15 @@ func listRollouts(ctx context.Context, c client.Client, namespace string) ([]Wor } return result, nil } + +func listDeploymentConfigs(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) { + var list openshiftv1.DeploymentConfigList + if err := c.List(ctx, &list, client.InNamespace(namespace)); err != nil { + return nil, err + } + result := make([]WorkloadAccessor, len(list.Items)) + for i := range list.Items { + result[i] = NewDeploymentConfigWorkload(&list.Items[i]) + } + return result, nil +} diff --git a/internal/pkg/workload/registry.go b/internal/pkg/workload/registry.go index 696b11c5..3516338d 100644 --- a/internal/pkg/workload/registry.go +++ b/internal/pkg/workload/registry.go @@ -6,6 +6,7 @@ import ( "strings" argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + openshiftv1 "github.com/openshift/api/apps/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -14,16 +15,24 @@ import ( // WorkloadLister is a function that lists workloads of a specific kind. type WorkloadLister func(ctx context.Context, c client.Client, namespace string) ([]WorkloadAccessor, error) +// RegistryOptions configures the workload registry. +type RegistryOptions struct { + ArgoRolloutsEnabled bool + DeploymentConfigEnabled bool +} + // Registry provides factory methods for creating Workload instances. type Registry struct { - argoRolloutsEnabled bool - listers map[Kind]WorkloadLister + argoRolloutsEnabled bool + deploymentConfigEnabled bool + listers map[Kind]WorkloadLister } // NewRegistry creates a new workload registry. -func NewRegistry(argoRolloutsEnabled bool) *Registry { +func NewRegistry(opts RegistryOptions) *Registry { r := &Registry{ - argoRolloutsEnabled: argoRolloutsEnabled, + argoRolloutsEnabled: opts.ArgoRolloutsEnabled, + deploymentConfigEnabled: opts.DeploymentConfigEnabled, listers: map[Kind]WorkloadLister{ KindDeployment: listDeployments, KindDaemonSet: listDaemonSets, @@ -32,9 +41,12 @@ func NewRegistry(argoRolloutsEnabled bool) *Registry { KindCronJob: listCronJobs, }, } - if argoRolloutsEnabled { + if opts.ArgoRolloutsEnabled { r.listers[KindArgoRollout] = listRollouts } + if opts.DeploymentConfigEnabled { + r.listers[KindDeploymentConfig] = listDeploymentConfigs + } return r } @@ -55,6 +67,9 @@ func (r *Registry) SupportedKinds() []Kind { if r.argoRolloutsEnabled { kinds = append(kinds, KindArgoRollout) } + if r.deploymentConfigEnabled { + kinds = append(kinds, KindDeploymentConfig) + } return kinds } @@ -76,6 +91,11 @@ func (r *Registry) FromObject(obj client.Object) (WorkloadAccessor, error) { return nil, fmt.Errorf("argo Rollouts support is not enabled") } return NewRolloutWorkload(o), nil + case *openshiftv1.DeploymentConfig: + if !r.deploymentConfigEnabled { + return nil, fmt.Errorf("openShift DeploymentConfig support is not enabled") + } + return NewDeploymentConfigWorkload(o), nil default: return nil, fmt.Errorf("unsupported object type: %T", obj) } @@ -84,18 +104,20 @@ func (r *Registry) FromObject(obj client.Object) (WorkloadAccessor, error) { // kindAliases maps string representations to Kind constants. // Supports lowercase, title case, and plural forms for user convenience. var kindAliases = map[string]Kind{ - "deployment": KindDeployment, - "deployments": KindDeployment, - "daemonset": KindDaemonSet, - "daemonsets": KindDaemonSet, - "statefulset": KindStatefulSet, - "statefulsets": KindStatefulSet, - "rollout": KindArgoRollout, - "rollouts": KindArgoRollout, - "job": KindJob, - "jobs": KindJob, - "cronjob": KindCronJob, - "cronjobs": KindCronJob, + "deployment": KindDeployment, + "deployments": KindDeployment, + "daemonset": KindDaemonSet, + "daemonsets": KindDaemonSet, + "statefulset": KindStatefulSet, + "statefulsets": KindStatefulSet, + "rollout": KindArgoRollout, + "rollouts": KindArgoRollout, + "job": KindJob, + "jobs": KindJob, + "cronjob": KindCronJob, + "cronjobs": KindCronJob, + "deploymentconfig": KindDeploymentConfig, + "deploymentconfigs": KindDeploymentConfig, } // KindFromString converts a string to a Kind. diff --git a/internal/pkg/workload/registry_test.go b/internal/pkg/workload/registry_test.go index e681438d..b84830fa 100644 --- a/internal/pkg/workload/registry_test.go +++ b/internal/pkg/workload/registry_test.go @@ -4,6 +4,7 @@ import ( "testing" argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" + openshiftv1 "github.com/openshift/api/apps/v1" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" @@ -11,7 +12,7 @@ import ( ) func TestNewRegistry_WithoutArgoRollouts(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{ArgoRolloutsEnabled: false}) kinds := r.SupportedKinds() if len(kinds) != 5 { @@ -30,7 +31,7 @@ func TestNewRegistry_WithoutArgoRollouts(t *testing.T) { } func TestNewRegistry_WithArgoRollouts(t *testing.T) { - r := NewRegistry(true) + r := NewRegistry(RegistryOptions{ArgoRolloutsEnabled: true}) kinds := r.SupportedKinds() if len(kinds) != 6 { @@ -54,7 +55,7 @@ func TestNewRegistry_WithArgoRollouts(t *testing.T) { } func TestRegistry_ListerFor_AllKinds(t *testing.T) { - r := NewRegistry(true) + r := NewRegistry(RegistryOptions{ArgoRolloutsEnabled: true}) tests := []struct { kind Kind @@ -78,7 +79,7 @@ func TestRegistry_ListerFor_AllKinds(t *testing.T) { } func TestRegistry_FromObject_Deployment(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) deploy := &appsv1.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -93,7 +94,7 @@ func TestRegistry_FromObject_Deployment(t *testing.T) { } func TestRegistry_FromObject_DaemonSet(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) ds := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -108,7 +109,7 @@ func TestRegistry_FromObject_DaemonSet(t *testing.T) { } func TestRegistry_FromObject_StatefulSet(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) sts := &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -123,7 +124,7 @@ func TestRegistry_FromObject_StatefulSet(t *testing.T) { } func TestRegistry_FromObject_Job(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -138,7 +139,7 @@ func TestRegistry_FromObject_Job(t *testing.T) { } func TestRegistry_FromObject_CronJob(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) cj := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -153,7 +154,7 @@ func TestRegistry_FromObject_CronJob(t *testing.T) { } func TestRegistry_FromObject_Rollout_Enabled(t *testing.T) { - r := NewRegistry(true) + r := NewRegistry(RegistryOptions{ArgoRolloutsEnabled: true}) rollout := &argorolloutv1alpha1.Rollout{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -168,7 +169,7 @@ func TestRegistry_FromObject_Rollout_Enabled(t *testing.T) { } func TestRegistry_FromObject_Rollout_Disabled(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) rollout := &argorolloutv1alpha1.Rollout{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -180,7 +181,7 @@ func TestRegistry_FromObject_Rollout_Disabled(t *testing.T) { } func TestRegistry_FromObject_UnsupportedType(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) cm := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, } @@ -234,7 +235,7 @@ func TestKindFromString(t *testing.T) { } func TestNewLister(t *testing.T) { - r := NewRegistry(false) + r := NewRegistry(RegistryOptions{}) l := NewLister(nil, r, nil) if l == nil { @@ -244,3 +245,122 @@ func TestNewLister(t *testing.T) { t.Error("NewLister should set Registry") } } + +// DeploymentConfig registry tests +func TestNewRegistry_WithDeploymentConfig(t *testing.T) { + r := NewRegistry(RegistryOptions{DeploymentConfigEnabled: true}) + + kinds := r.SupportedKinds() + if len(kinds) != 6 { + t.Errorf("SupportedKinds() = %d kinds, want 6", len(kinds)) + } + + found := false + for _, k := range kinds { + if k == KindDeploymentConfig { + found = true + break + } + } + if !found { + t.Error("SupportedKinds() should include DeploymentConfig when enabled") + } + + if r.ListerFor(KindDeploymentConfig) == nil { + t.Error("ListerFor(KindDeploymentConfig) should return a function when enabled") + } +} + +func TestNewRegistry_WithoutDeploymentConfig(t *testing.T) { + r := NewRegistry(RegistryOptions{DeploymentConfigEnabled: false}) + + for _, k := range r.SupportedKinds() { + if k == KindDeploymentConfig { + t.Error("SupportedKinds() should not include DeploymentConfig when disabled") + } + } + + if r.ListerFor(KindDeploymentConfig) != nil { + t.Error("ListerFor(KindDeploymentConfig) should return nil when disabled") + } +} + +func TestNewRegistry_WithBothOptionalWorkloads(t *testing.T) { + r := NewRegistry(RegistryOptions{ + ArgoRolloutsEnabled: true, + DeploymentConfigEnabled: true, + }) + + kinds := r.SupportedKinds() + if len(kinds) != 7 { + t.Errorf("SupportedKinds() = %d kinds, want 7 (5 base + ArgoRollout + DeploymentConfig)", len(kinds)) + } + + foundRollout := false + foundDC := false + for _, k := range kinds { + if k == KindArgoRollout { + foundRollout = true + } + if k == KindDeploymentConfig { + foundDC = true + } + } + if !foundRollout { + t.Error("SupportedKinds() should include ArgoRollout") + } + if !foundDC { + t.Error("SupportedKinds() should include DeploymentConfig") + } +} + +func TestRegistry_FromObject_DeploymentConfig_Enabled(t *testing.T) { + r := NewRegistry(RegistryOptions{DeploymentConfigEnabled: true}) + dc := &openshiftv1.DeploymentConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + } + + w, err := r.FromObject(dc) + if err != nil { + t.Fatalf("FromObject(DeploymentConfig) error = %v", err) + } + if w.Kind() != KindDeploymentConfig { + t.Errorf("FromObject(DeploymentConfig).Kind() = %v, want %v", w.Kind(), KindDeploymentConfig) + } +} + +func TestRegistry_FromObject_DeploymentConfig_Disabled(t *testing.T) { + r := NewRegistry(RegistryOptions{DeploymentConfigEnabled: false}) + dc := &openshiftv1.DeploymentConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "test", Namespace: "default"}, + } + + _, err := r.FromObject(dc) + if err == nil { + t.Error("FromObject(DeploymentConfig) should return error when DeploymentConfig disabled") + } +} + +func TestKindFromString_DeploymentConfig(t *testing.T) { + tests := []struct { + input string + want Kind + wantErr bool + }{ + {"deploymentconfig", KindDeploymentConfig, false}, + {"deploymentconfigs", KindDeploymentConfig, false}, + {"DeploymentConfig", KindDeploymentConfig, false}, + {"DEPLOYMENTCONFIG", KindDeploymentConfig, false}, + } + + for _, tt := range tests { + got, err := KindFromString(tt.input) + if (err != nil) != tt.wantErr { + t.Errorf("KindFromString(%q) error = %v, wantErr %v", tt.input, err, tt.wantErr) + continue + } + if got != tt.want { + t.Errorf("KindFromString(%q) = %v, want %v", tt.input, got, tt.want) + } + } +} diff --git a/internal/pkg/workload/workload_test.go b/internal/pkg/workload/workload_test.go index 674f7dbc..91139d85 100644 --- a/internal/pkg/workload/workload_test.go +++ b/internal/pkg/workload/workload_test.go @@ -4,22 +4,43 @@ import ( "testing" argorolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/stakater/Reloader/internal/pkg/testutil" ) -func TestDeploymentWorkload_BasicGetters(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-deploy", - Namespace: "test-ns", - Annotations: map[string]string{ - "key": "value", +// addEnvVar adds an environment variable with a ConfigMapKeyRef or SecretKeyRef to a container. +func addEnvVarConfigMapRef(containers []corev1.Container, envName, configMapName, key string) { + if len(containers) > 0 { + containers[0].Env = append(containers[0].Env, corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: configMapName}, + Key: key, + }, }, - }, + }) } +} + +func addEnvVarSecretRef(containers []corev1.Container, envName, secretName, key string) { + if len(containers) > 0 { + containers[0].Env = append(containers[0].Env, corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{Name: secretName}, + Key: key, + }, + }, + }) + } +} + +func TestDeploymentWorkload_BasicGetters(t *testing.T) { + deploy := testutil.NewDeployment("test-deploy", "test-ns", map[string]string{"key": "value"}) w := NewDeploymentWorkload(deploy) @@ -41,18 +62,8 @@ func TestDeploymentWorkload_BasicGetters(t *testing.T) { } func TestDeploymentWorkload_PodTemplateAnnotations(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "existing": "annotation", - }, - }, - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Annotations["existing"] = "annotation" w := NewDeploymentWorkload(deploy) @@ -70,14 +81,8 @@ func TestDeploymentWorkload_PodTemplateAnnotations(t *testing.T) { } func TestDeploymentWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - // No annotations set - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Annotations = nil w := NewDeploymentWorkload(deploy) @@ -95,21 +100,8 @@ func TestDeploymentWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { } func TestDeploymentWorkload_Containers(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - InitContainers: []corev1.Container{ - {Name: "init", Image: "busybox"}, - }, - }, - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Spec.InitContainers = []corev1.Container{{Name: "init", Image: "busybox"}} w := NewDeploymentWorkload(deploy) @@ -141,18 +133,10 @@ func TestDeploymentWorkload_Containers(t *testing.T) { } func TestDeploymentWorkload_Volumes(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - {Name: "config-vol"}, - {Name: "secret-vol"}, - }, - }, - }, - }, + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Spec.Volumes = []corev1.Volume{ + {Name: "config-vol"}, + {Name: "secret-vol"}, } w := NewDeploymentWorkload(deploy) @@ -164,27 +148,7 @@ func TestDeploymentWorkload_Volumes(t *testing.T) { } func TestDeploymentWorkload_UsesConfigMap_Volume(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "config-vol", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "my-config", - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithVolume("test", "default", "my-config", "") w := NewDeploymentWorkload(deploy) @@ -197,33 +161,7 @@ func TestDeploymentWorkload_UsesConfigMap_Volume(t *testing.T) { } func TestDeploymentWorkload_UsesConfigMap_ProjectedVolume(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "projected-vol", - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: []corev1.VolumeProjection{ - { - ConfigMap: &corev1.ConfigMapProjection{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "projected-config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithProjectedVolume("test", "default", "projected-config", "") w := NewDeploymentWorkload(deploy) @@ -233,29 +171,7 @@ func TestDeploymentWorkload_UsesConfigMap_ProjectedVolume(t *testing.T) { } func TestDeploymentWorkload_UsesConfigMap_EnvFrom(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "env-config", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithEnvFrom("test", "default", "env-config", "") w := NewDeploymentWorkload(deploy) @@ -265,33 +181,8 @@ func TestDeploymentWorkload_UsesConfigMap_EnvFrom(t *testing.T) { } func TestDeploymentWorkload_UsesConfigMap_EnvVar(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - Env: []corev1.EnvVar{ - { - Name: "CONFIG_VALUE", - ValueFrom: &corev1.EnvVarSource{ - ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "var-config", - }, - Key: "some-key", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) + addEnvVarConfigMapRef(deploy.Spec.Template.Spec.Containers, "CONFIG_VALUE", "var-config", "some-key") w := NewDeploymentWorkload(deploy) @@ -301,24 +192,14 @@ func TestDeploymentWorkload_UsesConfigMap_EnvVar(t *testing.T) { } func TestDeploymentWorkload_UsesConfigMap_InitContainer(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{ - { - Name: "init", - EnvFrom: []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "init-config", - }, - }, - }, - }, - }, + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "init-config"}, }, }, }, @@ -333,25 +214,7 @@ func TestDeploymentWorkload_UsesConfigMap_InitContainer(t *testing.T) { } func TestDeploymentWorkload_UsesSecret_Volume(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "secret-vol", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: "my-secret", - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithVolume("test", "default", "", "my-secret") w := NewDeploymentWorkload(deploy) @@ -364,33 +227,7 @@ func TestDeploymentWorkload_UsesSecret_Volume(t *testing.T) { } func TestDeploymentWorkload_UsesSecret_ProjectedVolume(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "projected-vol", - VolumeSource: corev1.VolumeSource{ - Projected: &corev1.ProjectedVolumeSource{ - Sources: []corev1.VolumeProjection{ - { - Secret: &corev1.SecretProjection{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "projected-secret", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithProjectedVolume("test", "default", "", "projected-secret") w := NewDeploymentWorkload(deploy) @@ -400,29 +237,7 @@ func TestDeploymentWorkload_UsesSecret_ProjectedVolume(t *testing.T) { } func TestDeploymentWorkload_UsesSecret_EnvFrom(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "env-secret", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeploymentWithEnvFrom("test", "default", "", "env-secret") w := NewDeploymentWorkload(deploy) @@ -432,33 +247,8 @@ func TestDeploymentWorkload_UsesSecret_EnvFrom(t *testing.T) { } func TestDeploymentWorkload_UsesSecret_EnvVar(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - Env: []corev1.EnvVar{ - { - Name: "SECRET_VALUE", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "var-secret", - }, - Key: "some-key", - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) + addEnvVarSecretRef(deploy.Spec.Template.Spec.Containers, "SECRET_VALUE", "var-secret", "some-key") w := NewDeploymentWorkload(deploy) @@ -468,24 +258,14 @@ func TestDeploymentWorkload_UsesSecret_EnvVar(t *testing.T) { } func TestDeploymentWorkload_UsesSecret_InitContainer(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - InitContainers: []corev1.Container{ - { - Name: "init", - EnvFrom: []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "init-secret", - }, - }, - }, - }, - }, + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "init-secret"}, }, }, }, @@ -500,35 +280,21 @@ func TestDeploymentWorkload_UsesSecret_InitContainer(t *testing.T) { } func TestDeploymentWorkload_GetEnvFromSources(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, - }, - }, - { - Name: "sidecar", - EnvFrom: []corev1.EnvFromSource{ - {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: "init", - EnvFrom: []corev1.EnvFromSource{ - {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "init-cm"}}}, - }, - }, - }, - }, - }, + deploy := testutil.NewDeployment("test", "default", nil) + deploy.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "main", + EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}}, + }, + { + Name: "sidecar", + EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}}, + }, + } + deploy.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{{ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "init-cm"}}}}, }, } @@ -541,21 +307,7 @@ func TestDeploymentWorkload_GetEnvFromSources(t *testing.T) { } func TestDeploymentWorkload_DeepCopy(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: appsv1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - }, - }, - }, - } + deploy := testutil.NewDeployment("test", "default", nil) w := NewDeploymentWorkload(deploy) copy := w.DeepCopy() @@ -571,17 +323,9 @@ func TestDeploymentWorkload_DeepCopy(t *testing.T) { } func TestDeploymentWorkload_GetOwnerReferences(t *testing.T) { - deploy := &appsv1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "apps/v1", - Kind: "ReplicaSet", - Name: "test-rs", - }, - }, - }, + deploy := testutil.NewDeployment("test", "default", nil) + deploy.OwnerReferences = []metav1.OwnerReference{ + {APIVersion: "apps/v1", Kind: "ReplicaSet", Name: "test-rs"}, } w := NewDeploymentWorkload(deploy) @@ -594,15 +338,7 @@ func TestDeploymentWorkload_GetOwnerReferences(t *testing.T) { // DaemonSet tests func TestDaemonSetWorkload_BasicGetters(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-ds", - Namespace: "test-ns", - Annotations: map[string]string{ - "key": "value", - }, - }, - } + ds := testutil.NewDaemonSet("test-ds", "test-ns", map[string]string{"key": "value"}) w := NewDaemonSetWorkload(ds) @@ -624,18 +360,8 @@ func TestDaemonSetWorkload_BasicGetters(t *testing.T) { } func TestDaemonSetWorkload_PodTemplateAnnotations(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "existing": "annotation", - }, - }, - }, - }, - } + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Annotations["existing"] = "annotation" w := NewDaemonSetWorkload(ds) @@ -651,12 +377,8 @@ func TestDaemonSetWorkload_PodTemplateAnnotations(t *testing.T) { } func TestDaemonSetWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{}, - }, - } + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Annotations = nil w := NewDaemonSetWorkload(ds) @@ -672,21 +394,8 @@ func TestDaemonSetWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { } func TestDaemonSetWorkload_Containers(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - InitContainers: []corev1.Container{ - {Name: "init", Image: "busybox"}, - }, - }, - }, - }, - } + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.InitContainers = []corev1.Container{{Name: "init", Image: "busybox"}} w := NewDaemonSetWorkload(ds) @@ -714,18 +423,10 @@ func TestDaemonSetWorkload_Containers(t *testing.T) { } func TestDaemonSetWorkload_Volumes(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - {Name: "config-vol"}, - {Name: "secret-vol"}, - }, - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.Volumes = []corev1.Volume{ + {Name: "config-vol"}, + {Name: "secret-vol"}, } w := NewDaemonSetWorkload(ds) @@ -737,23 +438,13 @@ func TestDaemonSetWorkload_Volumes(t *testing.T) { } func TestDaemonSetWorkload_UsesConfigMap(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "config-vol", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "ds-config", - }, - }, - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "ds-config"}, }, }, }, @@ -770,28 +461,9 @@ func TestDaemonSetWorkload_UsesConfigMap(t *testing.T) { } func TestDaemonSetWorkload_UsesConfigMap_EnvFrom(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "ds-env-config", - }, - }, - }, - }, - }, - }, - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "ds-env-config"}}}, } w := NewDaemonSetWorkload(ds) @@ -802,22 +474,12 @@ func TestDaemonSetWorkload_UsesConfigMap_EnvFrom(t *testing.T) { } func TestDaemonSetWorkload_UsesSecret(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "secret-vol", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: "ds-secret", - }, - }, - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "secret-vol", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{SecretName: "ds-secret"}, }, }, } @@ -833,29 +495,14 @@ func TestDaemonSetWorkload_UsesSecret(t *testing.T) { } func TestDaemonSetWorkload_GetEnvFromSources(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: "init", - EnvFrom: []corev1.EnvFromSource{ - {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}, - }, - }, - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, + } + ds.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}}, }, } @@ -868,21 +515,7 @@ func TestDaemonSetWorkload_GetEnvFromSources(t *testing.T) { } func TestDaemonSetWorkload_DeepCopy(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: appsv1.DaemonSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - }, - }, - }, - } + ds := testutil.NewDaemonSet("test", "default", nil) w := NewDaemonSetWorkload(ds) copy := w.DeepCopy() @@ -896,17 +529,9 @@ func TestDaemonSetWorkload_DeepCopy(t *testing.T) { } func TestDaemonSetWorkload_GetOwnerReferences(t *testing.T) { - ds := &appsv1.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "apps/v1", - Kind: "DaemonSet", - Name: "test-owner", - }, - }, - }, + ds := testutil.NewDaemonSet("test", "default", nil) + ds.OwnerReferences = []metav1.OwnerReference{ + {APIVersion: "apps/v1", Kind: "DaemonSet", Name: "test-owner"}, } w := NewDaemonSetWorkload(ds) @@ -919,15 +544,7 @@ func TestDaemonSetWorkload_GetOwnerReferences(t *testing.T) { // StatefulSet tests func TestStatefulSetWorkload_BasicGetters(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-sts", - Namespace: "test-ns", - Annotations: map[string]string{ - "key": "value", - }, - }, - } + sts := testutil.NewStatefulSet("test-sts", "test-ns", map[string]string{"key": "value"}) w := NewStatefulSetWorkload(sts) @@ -949,18 +566,8 @@ func TestStatefulSetWorkload_BasicGetters(t *testing.T) { } func TestStatefulSetWorkload_PodTemplateAnnotations(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "existing": "annotation", - }, - }, - }, - }, - } + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Annotations["existing"] = "annotation" w := NewStatefulSetWorkload(sts) @@ -976,12 +583,8 @@ func TestStatefulSetWorkload_PodTemplateAnnotations(t *testing.T) { } func TestStatefulSetWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{}, - }, - } + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Annotations = nil w := NewStatefulSetWorkload(sts) @@ -997,21 +600,8 @@ func TestStatefulSetWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { } func TestStatefulSetWorkload_Containers(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - InitContainers: []corev1.Container{ - {Name: "init", Image: "busybox"}, - }, - }, - }, - }, - } + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.InitContainers = []corev1.Container{{Name: "init", Image: "busybox"}} w := NewStatefulSetWorkload(sts) @@ -1039,18 +629,10 @@ func TestStatefulSetWorkload_Containers(t *testing.T) { } func TestStatefulSetWorkload_Volumes(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - {Name: "config-vol"}, - {Name: "secret-vol"}, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Volumes = []corev1.Volume{ + {Name: "config-vol"}, + {Name: "secret-vol"}, } w := NewStatefulSetWorkload(sts) @@ -1062,23 +644,13 @@ func TestStatefulSetWorkload_Volumes(t *testing.T) { } func TestStatefulSetWorkload_UsesConfigMap(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "config-vol", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "sts-config", - }, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "sts-config"}, }, }, }, @@ -1095,28 +667,9 @@ func TestStatefulSetWorkload_UsesConfigMap(t *testing.T) { } func TestStatefulSetWorkload_UsesConfigMap_EnvFrom(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - ConfigMapRef: &corev1.ConfigMapEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "sts-env-config", - }, - }, - }, - }, - }, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "sts-env-config"}}}, } w := NewStatefulSetWorkload(sts) @@ -1127,22 +680,12 @@ func TestStatefulSetWorkload_UsesConfigMap_EnvFrom(t *testing.T) { } func TestStatefulSetWorkload_UsesSecret(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "secret-vol", - VolumeSource: corev1.VolumeSource{ - Secret: &corev1.SecretVolumeSource{ - SecretName: "sts-secret", - }, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "secret-vol", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{SecretName: "sts-secret"}, }, }, } @@ -1158,28 +701,9 @@ func TestStatefulSetWorkload_UsesSecret(t *testing.T) { } func TestStatefulSetWorkload_UsesSecret_EnvFrom(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "sts-env-secret", - }, - }, - }, - }, - }, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "sts-env-secret"}}}, } w := NewStatefulSetWorkload(sts) @@ -1190,29 +714,14 @@ func TestStatefulSetWorkload_UsesSecret_EnvFrom(t *testing.T) { } func TestStatefulSetWorkload_GetEnvFromSources(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, - }, - }, - }, - InitContainers: []corev1.Container{ - { - Name: "init", - EnvFrom: []corev1.EnvFromSource{ - {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}, - }, - }, - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, + } + sts.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{{SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}}, }, } @@ -1225,21 +734,7 @@ func TestStatefulSetWorkload_GetEnvFromSources(t *testing.T) { } func TestStatefulSetWorkload_DeepCopy(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - Namespace: "default", - }, - Spec: appsv1.StatefulSetSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - {Name: "main", Image: "nginx"}, - }, - }, - }, - }, - } + sts := testutil.NewStatefulSet("test", "default", nil) w := NewStatefulSetWorkload(sts) copy := w.DeepCopy() @@ -1253,17 +748,9 @@ func TestStatefulSetWorkload_DeepCopy(t *testing.T) { } func TestStatefulSetWorkload_GetOwnerReferences(t *testing.T) { - sts := &appsv1.StatefulSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test", - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "apps/v1", - Kind: "StatefulSet", - Name: "test-owner", - }, - }, - }, + sts := testutil.NewStatefulSet("test", "default", nil) + sts.OwnerReferences = []metav1.OwnerReference{ + {APIVersion: "apps/v1", Kind: "StatefulSet", Name: "test-owner"}, } w := NewStatefulSetWorkload(sts) @@ -1505,15 +992,7 @@ func TestToRolloutStrategy(t *testing.T) { // Job tests func TestJobWorkload_BasicGetters(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-job", - Namespace: "test-ns", - Annotations: map[string]string{ - "key": "value", - }, - }, - } + job := testutil.NewJobWithAnnotations("test-job", "test-ns", map[string]string{"key": "value"}) w := NewJobWorkload(job) @@ -1535,18 +1014,8 @@ func TestJobWorkload_BasicGetters(t *testing.T) { } func TestJobWorkload_PodTemplateAnnotations(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "existing": "annotation", - }, - }, - }, - }, - } + job := testutil.NewJob("test", "default") + job.Spec.Template.Annotations["existing"] = "annotation" w := NewJobWorkload(job) @@ -1562,23 +1031,13 @@ func TestJobWorkload_PodTemplateAnnotations(t *testing.T) { } func TestJobWorkload_UsesConfigMap(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "config-vol", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "job-config", - }, - }, - }, - }, - }, + job := testutil.NewJob("test", "default") + job.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "job-config"}, }, }, }, @@ -1595,26 +1054,11 @@ func TestJobWorkload_UsesConfigMap(t *testing.T) { } func TestJobWorkload_UsesSecret(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - EnvFrom: []corev1.EnvFromSource{ - { - SecretRef: &corev1.SecretEnvSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "job-secret", - }, - }, - }, - }, - }, - }, - }, + job := testutil.NewJob("test", "default") + job.Spec.Template.Spec.Containers[0].EnvFrom = []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "job-secret"}, }, }, } @@ -1627,18 +1071,8 @@ func TestJobWorkload_UsesSecret(t *testing.T) { } func TestJobWorkload_DeepCopy(t *testing.T) { - job := &batchv1.Job{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "original": "value", - }, - }, - }, - }, - } + job := testutil.NewJob("test", "default") + job.Spec.Template.Annotations["original"] = "value" w := NewJobWorkload(job) copy := w.DeepCopy() @@ -1653,15 +1087,7 @@ func TestJobWorkload_DeepCopy(t *testing.T) { // CronJob tests func TestCronJobWorkload_BasicGetters(t *testing.T) { - cj := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-cronjob", - Namespace: "test-ns", - Annotations: map[string]string{ - "key": "value", - }, - }, - } + cj := testutil.NewCronJobWithAnnotations("test-cronjob", "test-ns", map[string]string{"key": "value"}) w := NewCronJobWorkload(cj) @@ -1683,22 +1109,8 @@ func TestCronJobWorkload_BasicGetters(t *testing.T) { } func TestCronJobWorkload_PodTemplateAnnotations(t *testing.T) { - cj := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.CronJobSpec{ - JobTemplate: batchv1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "existing": "annotation", - }, - }, - }, - }, - }, - }, - } + cj := testutil.NewCronJob("test", "default") + cj.Spec.JobTemplate.Spec.Template.Annotations["existing"] = "annotation" w := NewCronJobWorkload(cj) @@ -1714,27 +1126,13 @@ func TestCronJobWorkload_PodTemplateAnnotations(t *testing.T) { } func TestCronJobWorkload_UsesConfigMap(t *testing.T) { - cj := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.CronJobSpec{ - JobTemplate: batchv1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Volumes: []corev1.Volume{ - { - Name: "config-vol", - VolumeSource: corev1.VolumeSource{ - ConfigMap: &corev1.ConfigMapVolumeSource{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "cronjob-config", - }, - }, - }, - }, - }, - }, - }, + cj := testutil.NewCronJob("test", "default") + cj.Spec.JobTemplate.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{Name: "cronjob-config"}, }, }, }, @@ -1751,37 +1149,8 @@ func TestCronJobWorkload_UsesConfigMap(t *testing.T) { } func TestCronJobWorkload_UsesSecret(t *testing.T) { - cj := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.CronJobSpec{ - JobTemplate: batchv1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Name: "main", - Env: []corev1.EnvVar{ - { - Name: "SECRET_VALUE", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: "cronjob-secret", - }, - Key: "key", - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - } + cj := testutil.NewCronJob("test", "default") + addEnvVarSecretRef(cj.Spec.JobTemplate.Spec.Template.Spec.Containers, "SECRET_VALUE", "cronjob-secret", "key") w := NewCronJobWorkload(cj) @@ -1791,22 +1160,8 @@ func TestCronJobWorkload_UsesSecret(t *testing.T) { } func TestCronJobWorkload_DeepCopy(t *testing.T) { - cj := &batchv1.CronJob{ - ObjectMeta: metav1.ObjectMeta{Name: "test"}, - Spec: batchv1.CronJobSpec{ - JobTemplate: batchv1.JobTemplateSpec{ - Spec: batchv1.JobSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{ - Annotations: map[string]string{ - "original": "value", - }, - }, - }, - }, - }, - }, - } + cj := testutil.NewCronJob("test", "default") + cj.Spec.JobTemplate.Spec.Template.Annotations["original"] = "value" w := NewCronJobWorkload(cj) copy := w.DeepCopy() @@ -1824,3 +1179,364 @@ func TestJobCronJobWorkloadInterface(t *testing.T) { var _ WorkloadAccessor = (*JobWorkload)(nil) var _ WorkloadAccessor = (*CronJobWorkload)(nil) } + +// DeploymentConfig tests +func TestDeploymentConfigWorkload_BasicGetters(t *testing.T) { + dc := testutil.NewDeploymentConfig("test-dc", "test-ns", map[string]string{"key": "value"}) + + w := NewDeploymentConfigWorkload(dc) + + if w.Kind() != KindDeploymentConfig { + t.Errorf("Kind() = %v, want %v", w.Kind(), KindDeploymentConfig) + } + if w.GetName() != "test-dc" { + t.Errorf("GetName() = %v, want test-dc", w.GetName()) + } + if w.GetNamespace() != "test-ns" { + t.Errorf("GetNamespace() = %v, want test-ns", w.GetNamespace()) + } + if w.GetAnnotations()["key"] != "value" { + t.Errorf("GetAnnotations()[key] = %v, want value", w.GetAnnotations()["key"]) + } + if w.GetObject() != dc { + t.Error("GetObject() should return the underlying deploymentconfig") + } +} + +func TestDeploymentConfigWorkload_PodTemplateAnnotations(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Annotations = map[string]string{"existing": "annotation"} + + w := NewDeploymentConfigWorkload(dc) + + annotations := w.GetPodTemplateAnnotations() + if annotations["existing"] != "annotation" { + t.Errorf("GetPodTemplateAnnotations()[existing] = %v, want annotation", annotations["existing"]) + } + + w.SetPodTemplateAnnotation("new-key", "new-value") + if w.GetPodTemplateAnnotations()["new-key"] != "new-value" { + t.Error("SetPodTemplateAnnotation should add new annotation") + } +} + +func TestDeploymentConfigWorkload_PodTemplateAnnotations_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + // Should handle nil template gracefully + annotations := w.GetPodTemplateAnnotations() + if annotations != nil { + t.Error("GetPodTemplateAnnotations should return nil for nil template") + } + + // SetPodTemplateAnnotation should initialize template + w.SetPodTemplateAnnotation("key", "value") + if w.GetPodTemplateAnnotations()["key"] != "value" { + t.Error("SetPodTemplateAnnotation should work with nil template") + } +} + +func TestDeploymentConfigWorkload_PodTemplateAnnotations_NilInit(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Annotations = nil + + w := NewDeploymentConfigWorkload(dc) + + // Should initialize nil map + annotations := w.GetPodTemplateAnnotations() + if annotations == nil { + t.Error("GetPodTemplateAnnotations should initialize nil map") + } + + w.SetPodTemplateAnnotation("key", "value") + if w.GetPodTemplateAnnotations()["key"] != "value" { + t.Error("SetPodTemplateAnnotation should work with nil initial map") + } +} + +func TestDeploymentConfigWorkload_Containers(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Containers = []corev1.Container{ + {Name: "main", Image: "nginx"}, + } + dc.Spec.Template.Spec.InitContainers = []corev1.Container{ + {Name: "init", Image: "busybox"}, + } + + w := NewDeploymentConfigWorkload(dc) + + containers := w.GetContainers() + if len(containers) != 1 || containers[0].Name != "main" { + t.Errorf("GetContainers() = %v, want [main]", containers) + } + + initContainers := w.GetInitContainers() + if len(initContainers) != 1 || initContainers[0].Name != "init" { + t.Errorf("GetInitContainers() = %v, want [init]", initContainers) + } + + newContainers := []corev1.Container{{Name: "new-main", Image: "alpine"}} + w.SetContainers(newContainers) + if w.GetContainers()[0].Name != "new-main" { + t.Error("SetContainers should update containers") + } + + newInitContainers := []corev1.Container{{Name: "new-init", Image: "alpine"}} + w.SetInitContainers(newInitContainers) + if w.GetInitContainers()[0].Name != "new-init" { + t.Error("SetInitContainers should update init containers") + } +} + +func TestDeploymentConfigWorkload_Containers_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + if w.GetContainers() != nil { + t.Error("GetContainers should return nil for nil template") + } + if w.GetInitContainers() != nil { + t.Error("GetInitContainers should return nil for nil template") + } + + // SetContainers should initialize template + w.SetContainers([]corev1.Container{{Name: "main"}}) + if len(w.GetContainers()) != 1 { + t.Error("SetContainers should work with nil template") + } +} + +func TestDeploymentConfigWorkload_Volumes(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Volumes = []corev1.Volume{ + {Name: "config-vol"}, + {Name: "secret-vol"}, + } + + w := NewDeploymentConfigWorkload(dc) + + volumes := w.GetVolumes() + if len(volumes) != 2 { + t.Errorf("GetVolumes() length = %d, want 2", len(volumes)) + } +} + +func TestDeploymentConfigWorkload_Volumes_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + if w.GetVolumes() != nil { + t.Error("GetVolumes should return nil for nil template") + } +} + +func TestDeploymentConfigWorkload_UsesConfigMap_Volume(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "config-vol", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "dc-config", + }, + }, + }, + }, + } + + w := NewDeploymentConfigWorkload(dc) + + if !w.UsesConfigMap("dc-config") { + t.Error("DeploymentConfig UsesConfigMap should return true for ConfigMap volume") + } + if w.UsesConfigMap("other-config") { + t.Error("UsesConfigMap should return false for non-existent ConfigMap") + } +} + +func TestDeploymentConfigWorkload_UsesConfigMap_EnvFrom(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "main", + EnvFrom: []corev1.EnvFromSource{ + { + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "dc-env-config", + }, + }, + }, + }, + }, + } + + w := NewDeploymentConfigWorkload(dc) + + if !w.UsesConfigMap("dc-env-config") { + t.Error("DeploymentConfig UsesConfigMap should return true for envFrom ConfigMap") + } +} + +func TestDeploymentConfigWorkload_UsesConfigMap_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + if w.UsesConfigMap("any-config") { + t.Error("UsesConfigMap should return false for nil template") + } +} + +func TestDeploymentConfigWorkload_UsesSecret_Volume(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Volumes = []corev1.Volume{ + { + Name: "secret-vol", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: "dc-secret", + }, + }, + }, + } + + w := NewDeploymentConfigWorkload(dc) + + if !w.UsesSecret("dc-secret") { + t.Error("DeploymentConfig UsesSecret should return true for Secret volume") + } + if w.UsesSecret("other-secret") { + t.Error("UsesSecret should return false for non-existent Secret") + } +} + +func TestDeploymentConfigWorkload_UsesSecret_EnvFrom(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "main", + EnvFrom: []corev1.EnvFromSource{ + { + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "dc-env-secret", + }, + }, + }, + }, + }, + } + + w := NewDeploymentConfigWorkload(dc) + + if !w.UsesSecret("dc-env-secret") { + t.Error("DeploymentConfig UsesSecret should return true for envFrom Secret") + } +} + +func TestDeploymentConfigWorkload_UsesSecret_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + if w.UsesSecret("any-secret") { + t.Error("UsesSecret should return false for nil template") + } +} + +func TestDeploymentConfigWorkload_GetEnvFromSources(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "main", + EnvFrom: []corev1.EnvFromSource{ + {ConfigMapRef: &corev1.ConfigMapEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "cm1"}}}, + }, + }, + } + dc.Spec.Template.Spec.InitContainers = []corev1.Container{ + { + Name: "init", + EnvFrom: []corev1.EnvFromSource{ + {SecretRef: &corev1.SecretEnvSource{LocalObjectReference: corev1.LocalObjectReference{Name: "secret1"}}}, + }, + }, + } + + w := NewDeploymentConfigWorkload(dc) + + sources := w.GetEnvFromSources() + if len(sources) != 2 { + t.Errorf("GetEnvFromSources() returned %d sources, want 2", len(sources)) + } +} + +func TestDeploymentConfigWorkload_GetEnvFromSources_NilTemplate(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template = nil + + w := NewDeploymentConfigWorkload(dc) + + if w.GetEnvFromSources() != nil { + t.Error("GetEnvFromSources should return nil for nil template") + } +} + +func TestDeploymentConfigWorkload_DeepCopy(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.Spec.Template.Annotations = map[string]string{"original": "value"} + + w := NewDeploymentConfigWorkload(dc) + copy := w.DeepCopy() + + w.SetPodTemplateAnnotation("modified", "true") + + copyAnnotations := copy.GetPodTemplateAnnotations() + if copyAnnotations["modified"] == "true" { + t.Error("DeepCopy should create independent copy") + } +} + +func TestDeploymentConfigWorkload_GetOwnerReferences(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + dc.OwnerReferences = []metav1.OwnerReference{ + { + APIVersion: "apps.openshift.io/v1", + Kind: "DeploymentConfig", + Name: "test-owner", + }, + } + + w := NewDeploymentConfigWorkload(dc) + + refs := w.GetOwnerReferences() + if len(refs) != 1 || refs[0].Name != "test-owner" { + t.Errorf("GetOwnerReferences() = %v, want owner ref to test-owner", refs) + } +} + +func TestDeploymentConfigWorkload_GetDeploymentConfig(t *testing.T) { + dc := testutil.NewDeploymentConfig("test", "default", nil) + + w := NewDeploymentConfigWorkload(dc) + + if w.GetDeploymentConfig() != dc { + t.Error("GetDeploymentConfig should return the underlying DeploymentConfig") + } +} + +// Test that DeploymentConfig implements the interface +func TestDeploymentConfigWorkloadInterface(t *testing.T) { + var _ WorkloadAccessor = (*DeploymentConfigWorkload)(nil) +} diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 6d3dcc4c..b8303d9d 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -10,12 +10,15 @@ import ( "time" "github.com/go-logr/zerologr" + openshiftclient "github.com/openshift/client-go/apps/clientset/versioned" "github.com/rs/zerolog" "github.com/stakater/Reloader/internal/pkg/config" "github.com/stakater/Reloader/internal/pkg/controller" "github.com/stakater/Reloader/internal/pkg/metrics" + "github.com/stakater/Reloader/internal/pkg/openshift" "github.com/stakater/Reloader/internal/pkg/testutil" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/discovery" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -32,12 +35,14 @@ const ( ) var ( - k8sClient kubernetes.Interface - cfg *config.Config - namespace string - skipE2ETests bool - cancelManager context.CancelFunc - restCfg *rest.Config + k8sClient kubernetes.Interface + osClient openshiftclient.Interface + cfg *config.Config + namespace string + skipE2ETests bool + skipDeploymentConfigTests bool + cancelManager context.CancelFunc + restCfg *rest.Config ) // testFixture provides a clean way to set up and tear down test resources. @@ -155,7 +160,7 @@ func (f *testFixture) assertDeploymentReloaded(name string, testCfg *config.Conf if testCfg == nil { testCfg = cfg } - updated, err := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg, waitTimeout) + updated, err := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg.Annotations.LastReloadedFrom, waitTimeout) if err != nil { f.t.Fatalf("Error waiting for deployment %s update: %v", name, err) } @@ -171,7 +176,7 @@ func (f *testFixture) assertDeploymentNotReloaded(name string, testCfg *config.C testCfg = cfg } time.Sleep(negativeTestTimeout) - updated, _ := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg, negativeTestTimeout) + updated, _ := testutil.WaitForDeploymentReloadedAnnotation(k8sClient, namespace, name, testCfg.Annotations.LastReloadedFrom, negativeTestTimeout) if updated { f.t.Errorf("Deployment %s should not have been updated", name) } @@ -180,7 +185,7 @@ func (f *testFixture) assertDeploymentNotReloaded(name string, testCfg *config.C // assertDaemonSetReloaded asserts that a daemonset was reloaded. func (f *testFixture) assertDaemonSetReloaded(name string) { f.t.Helper() - updated, err := testutil.WaitForDaemonSetReloadedAnnotation(k8sClient, namespace, name, cfg, waitTimeout) + updated, err := testutil.WaitForDaemonSetReloadedAnnotation(k8sClient, namespace, name, cfg.Annotations.LastReloadedFrom, waitTimeout) if err != nil { f.t.Fatalf("Error waiting for daemonset %s update: %v", name, err) } @@ -192,7 +197,7 @@ func (f *testFixture) assertDaemonSetReloaded(name string) { // assertStatefulSetReloaded asserts that a statefulset was reloaded. func (f *testFixture) assertStatefulSetReloaded(name string) { f.t.Helper() - updated, err := testutil.WaitForStatefulSetReloadedAnnotation(k8sClient, namespace, name, cfg, waitTimeout) + updated, err := testutil.WaitForStatefulSetReloadedAnnotation(k8sClient, namespace, name, cfg.Annotations.LastReloadedFrom, waitTimeout) if err != nil { f.t.Fatalf("Error waiting for statefulset %s update: %v", name, err) } @@ -201,6 +206,28 @@ func (f *testFixture) assertStatefulSetReloaded(name string) { } } +// createDeploymentConfig creates a DeploymentConfig and registers it for cleanup. +func (f *testFixture) createDeploymentConfig(name string, useConfigMap bool, annotations map[string]string) { + f.t.Helper() + _, err := testutil.CreateDeploymentConfig(osClient, name, namespace, useConfigMap, annotations) + if err != nil { + f.t.Fatalf("Failed to create DeploymentConfig %s: %v", name, err) + } + f.workloads = append(f.workloads, workloadInfo{name: name, kind: "deploymentconfig"}) +} + +// assertDeploymentConfigReloaded asserts that a DeploymentConfig was reloaded. +func (f *testFixture) assertDeploymentConfigReloaded(name string) { + f.t.Helper() + updated, err := testutil.WaitForDeploymentConfigReloadedAnnotation(osClient, namespace, name, cfg.Annotations.LastReloadedFrom, waitTimeout) + if err != nil { + f.t.Fatalf("Error waiting for DeploymentConfig %s update: %v", name, err) + } + if !updated { + f.t.Errorf("DeploymentConfig %s was not updated after resource change", name) + } +} + // cleanup removes all created resources. func (f *testFixture) cleanup() { for _, w := range f.workloads { @@ -211,6 +238,10 @@ func (f *testFixture) cleanup() { _ = testutil.DeleteDaemonSet(k8sClient, namespace, w.name) case "statefulset": _ = testutil.DeleteStatefulSet(k8sClient, namespace, w.name) + case "deploymentconfig": + if osClient != nil { + _ = testutil.DeleteDeploymentConfig(osClient, namespace, w.name) + } } } for _, name := range f.configMaps { @@ -266,6 +297,25 @@ func TestMain(m *testing.M) { cfg = config.NewDefault() cfg.AutoReloadAll = false + // Check if cluster supports DeploymentConfig + discoveryClient, err := discovery.NewDiscoveryClientForConfig(restCfg) + if err != nil { + skipDeploymentConfigTests = true + } else { + // Use a nop logger for detection + nopLog := ctrl.Log.WithName("dc-detection") + if openshift.HasDeploymentConfigSupport(discoveryClient, nopLog) { + cfg.DeploymentConfigEnabled = true + // Create OpenShift client for DeploymentConfig tests + osClient, err = testutil.NewOpenshiftClient(restCfg) + if err != nil { + skipDeploymentConfigTests = true + } + } else { + skipDeploymentConfigTests = true + } + } + _, cancelManager = startManagerWithConfig(cfg, restCfg) code := m.Run() @@ -285,6 +335,13 @@ func skipIfNoCluster(t *testing.T) { } } +func skipIfNoDeploymentConfig(t *testing.T) { + skipIfNoCluster(t) + if skipDeploymentConfigTests { + t.Skip("Skipping DeploymentConfig test: cluster does not support DeploymentConfig API") + } +} + // TestConfigMapUpdate tests that updating a ConfigMap triggers a workload reload. func TestConfigMapUpdate(t *testing.T) { f := newFixture(t, "cm-update") @@ -497,6 +554,67 @@ func TestAutoWithBothExplicitAndReferencedChange(t *testing.T) { f.assertDeploymentReloaded(referencedCM, nil) } +// newFixtureForDeploymentConfig creates a new test fixture for DeploymentConfig tests. +func newFixtureForDeploymentConfig(t *testing.T, prefix string) *testFixture { + t.Helper() + skipIfNoDeploymentConfig(t) + return &testFixture{ + t: t, + name: prefix + "-" + testutil.RandSeq(5), + } +} + +// TestDeploymentConfigReloadConfigMap tests that updating a ConfigMap triggers a DeploymentConfig reload. +func TestDeploymentConfigReloadConfigMap(t *testing.T) { + f := newFixtureForDeploymentConfig(t, "dc-cm-reload") + defer f.cleanup() + + f.createConfigMap(f.name, "initial-data") + f.createDeploymentConfig( + f.name, true, map[string]string{ + cfg.Annotations.ConfigmapReload: f.name, + }, + ) + f.waitForReady() + + f.updateConfigMap(f.name, "updated-data") + f.assertDeploymentConfigReloaded(f.name) +} + +// TestDeploymentConfigReloadSecret tests that updating a Secret triggers a DeploymentConfig reload. +func TestDeploymentConfigReloadSecret(t *testing.T) { + f := newFixtureForDeploymentConfig(t, "dc-secret-reload") + defer f.cleanup() + + f.createSecret(f.name, "initial-secret") + f.createDeploymentConfig( + f.name, false, map[string]string{ + cfg.Annotations.SecretReload: f.name, + }, + ) + f.waitForReady() + + f.updateSecret(f.name, "updated-secret") + f.assertDeploymentConfigReloaded(f.name) +} + +// TestDeploymentConfigAutoReload tests the auto-reload annotation on DeploymentConfig. +func TestDeploymentConfigAutoReload(t *testing.T) { + f := newFixtureForDeploymentConfig(t, "dc-auto-reload") + defer f.cleanup() + + f.createConfigMap(f.name, "initial-data") + f.createDeploymentConfig( + f.name, true, map[string]string{ + cfg.Annotations.Auto: "true", + }, + ) + f.waitForReady() + + f.updateConfigMap(f.name, "updated-data") + f.assertDeploymentConfigReloaded(f.name) +} + // startManagerWithConfig creates and starts a controller-runtime manager for e2e testing. func startManagerWithConfig(cfg *config.Config, restConfig *rest.Config) (manager.Manager, context.CancelFunc) { collectors := metrics.NewCollectors()