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
+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
}