feat: DeploymentConfig support

This commit is contained in:
TheiLLeniumStudios
2025-12-28 16:43:28 +01:00
parent 9f331cac4f
commit fa5f1858f1
17 changed files with 1442 additions and 1040 deletions
+12
View File
@@ -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)
}
+4 -3
View File
@@ -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
+10 -1
View File
@@ -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=
+34 -32
View File
@@ -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,
+21
View File
@@ -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 == "" {
+6 -1
View File
@@ -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)
+8 -2
View File
@@ -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)),
+34
View File
@@ -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
}
+155 -53
View File
@@ -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{
+137 -60
View File
@@ -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
}
+143
View File
@@ -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
}
+7 -6
View File
@@ -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.
+13
View File
@@ -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
}
+39 -17
View File
@@ -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.
+132 -12
View File
@@ -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)
}
}
}
File diff suppressed because it is too large Load Diff
+128 -10
View File
@@ -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()