Files
Reloader/test/e2e/utils/resources.go
2026-01-14 19:41:04 +01:00

1008 lines
31 KiB
Go

package utils
import (
"context"
"fmt"
"strings"
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/client-go/kubernetes"
"k8s.io/utils/ptr"
)
const (
// DefaultImage is the default container image used for test workloads.
DefaultImage = "busybox:1.36"
// DefaultCommand is the default command for test containers.
DefaultCommand = "sleep 3600"
)
// CreateNamespace creates a namespace with the given name.
func CreateNamespace(ctx context.Context, client kubernetes.Interface, name string) error {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
_, err := client.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
return err
}
// CreateNamespaceWithLabels creates a namespace with the given name and labels.
func CreateNamespaceWithLabels(ctx context.Context, client kubernetes.Interface, name string, labels map[string]string) error {
ns := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: labels,
},
}
_, err := client.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})
return err
}
// DeleteNamespace deletes the namespace with the given name.
func DeleteNamespace(ctx context.Context, client kubernetes.Interface, name string) error {
return client.CoreV1().Namespaces().Delete(ctx, name, metav1.DeleteOptions{})
}
// CreateConfigMap creates a ConfigMap with the given name, data, and optional annotations.
func CreateConfigMap(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string]string, annotations map[string]string) (*corev1.ConfigMap, error) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: annotations,
},
Data: data,
}
return client.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{})
}
// CreateConfigMapWithLabels creates a ConfigMap with the given name, data, labels, and optional annotations.
func CreateConfigMapWithLabels(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string]string, labels, annotations map[string]string) (*corev1.ConfigMap, error) {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: labels,
Annotations: annotations,
},
Data: data,
}
return client.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{})
}
// CreateSecret creates a Secret with the given name, data, and optional annotations.
func CreateSecret(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string][]byte, annotations map[string]string) (*corev1.Secret, error) {
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Annotations: annotations,
},
Data: data,
}
return client.CoreV1().Secrets(namespace).Create(ctx, secret, metav1.CreateOptions{})
}
// UpdateConfigMap updates a ConfigMap's data.
func UpdateConfigMap(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string]string) error {
cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
cm.Data = data
_, err = client.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{})
return err
}
// UpdateConfigMapLabels updates a ConfigMap's labels.
func UpdateConfigMapLabels(ctx context.Context, client kubernetes.Interface, namespace, name string, labels map[string]string) error {
cm, err := client.CoreV1().ConfigMaps(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
if cm.Labels == nil {
cm.Labels = make(map[string]string)
}
for k, v := range labels {
cm.Labels[k] = v
}
_, err = client.CoreV1().ConfigMaps(namespace).Update(ctx, cm, metav1.UpdateOptions{})
return err
}
// UpdateSecret updates a Secret's data.
func UpdateSecret(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string][]byte) error {
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
secret.Data = data
_, err = client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
}
// UpdateSecretLabels updates a Secret's labels.
func UpdateSecretLabels(ctx context.Context, client kubernetes.Interface, namespace, name string, labels map[string]string) error {
secret, err := client.CoreV1().Secrets(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return err
}
if secret.Labels == nil {
secret.Labels = make(map[string]string)
}
for k, v := range labels {
secret.Labels[k] = v
}
_, err = client.CoreV1().Secrets(namespace).Update(ctx, secret, metav1.UpdateOptions{})
return err
}
// stringToByteMap converts a string map to a byte map for Secret data.
func stringToByteMap(data map[string]string) map[string][]byte {
result := make(map[string][]byte)
for k, v := range data {
result[k] = []byte(v)
}
return result
}
// CreateSecretFromStrings creates a Secret with string data (convenience wrapper).
func CreateSecretFromStrings(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string]string, annotations map[string]string) (*corev1.Secret, error) {
return CreateSecret(ctx, client, namespace, name, stringToByteMap(data), annotations)
}
// UpdateSecretFromStrings updates a Secret's data using string values.
func UpdateSecretFromStrings(ctx context.Context, client kubernetes.Interface, namespace, name string, data map[string]string) error {
return UpdateSecret(ctx, client, namespace, name, stringToByteMap(data))
}
// DeleteConfigMap deletes a ConfigMap.
func DeleteConfigMap(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.CoreV1().ConfigMaps(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// DeleteSecret deletes a Secret.
func DeleteSecret(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.CoreV1().Secrets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// DeploymentOption is a functional option for configuring a Deployment.
type DeploymentOption func(*appsv1.Deployment)
// CreateDeployment creates a Deployment with the given options.
func CreateDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...DeploymentOption) (*appsv1.Deployment, error) {
deploy := baseDeploymentResource(namespace, name)
for _, opt := range opts {
opt(deploy)
}
return client.AppsV1().Deployments(namespace).Create(ctx, deploy, metav1.CreateOptions{})
}
// WithAnnotations adds annotations to the Deployment metadata.
func WithAnnotations(annotations map[string]string) DeploymentOption {
return func(d *appsv1.Deployment) {
if d.Annotations == nil {
d.Annotations = make(map[string]string)
}
for k, v := range annotations {
d.Annotations[k] = v
}
}
}
// WithConfigMapEnvFrom adds an envFrom reference to a ConfigMap.
func WithConfigMapEnvFrom(name string) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
d.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// WithSecretEnvFrom adds an envFrom reference to a Secret.
func WithSecretEnvFrom(name string) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Template.Spec.Containers[0].EnvFrom = append(
d.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// WithConfigMapVolume adds a volume mount for a ConfigMap.
func WithConfigMapVolume(name string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := fmt.Sprintf("cm-%s", name)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
})
d.Spec.Template.Spec.Containers[0].VolumeMounts = append(
d.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: fmt.Sprintf("/etc/config/%s", name),
},
)
}
}
// WithSecretVolume adds a volume mount for a Secret.
func WithSecretVolume(name string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := fmt.Sprintf("secret-%s", name)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: name,
},
},
})
d.Spec.Template.Spec.Containers[0].VolumeMounts = append(
d.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: fmt.Sprintf("/etc/secrets/%s", name),
},
)
}
}
// WithProjectedVolume adds a projected volume with ConfigMap and/or Secret sources.
func WithProjectedVolume(cmName, secretName string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := "projected-config"
sources := []corev1.VolumeProjection{}
if cmName != "" {
sources = append(sources, corev1.VolumeProjection{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
},
})
}
if secretName != "" {
sources = append(sources, corev1.VolumeProjection{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
},
})
}
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: sources,
},
},
})
d.Spec.Template.Spec.Containers[0].VolumeMounts = append(
d.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: "/etc/projected",
},
)
}
}
// WithInitContainer adds an init container that references ConfigMap and/or Secret.
func WithInitContainer(cmName, secretName string) DeploymentOption {
return func(d *appsv1.Deployment) {
initContainer := corev1.Container{
Name: "init",
Image: DefaultImage,
Command: []string{"sh", "-c", "echo init done"},
}
if cmName != "" {
initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
},
})
}
if secretName != "" {
initContainer.EnvFrom = append(initContainer.EnvFrom, corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
},
})
}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, initContainer)
}
}
// WithMultipleContainers adds additional containers to the pod.
func WithMultipleContainers(count int) DeploymentOption {
return func(d *appsv1.Deployment) {
for i := 1; i < count; i++ {
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, corev1.Container{
Name: fmt.Sprintf("container-%d", i),
Image: DefaultImage,
Command: []string{"sh", "-c", DefaultCommand},
})
}
}
}
// WithMultipleContainersAndEnv creates two containers, each with a different ConfigMap envFrom.
func WithMultipleContainersAndEnv(cm1Name, cm2Name string) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Template.Spec.Containers[0].EnvFrom = append(d.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: cm1Name},
},
})
d.Spec.Template.Spec.Containers = append(d.Spec.Template.Spec.Containers, corev1.Container{
Name: "container-1",
Image: DefaultImage,
Command: []string{"sh", "-c", DefaultCommand},
EnvFrom: []corev1.EnvFromSource{
{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: cm2Name},
},
},
},
})
}
}
// WithReplicas sets the number of replicas.
func WithReplicas(replicas int32) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Replicas = ptr.To(replicas)
}
}
// WithConfigMapKeyRef adds a valueFrom.configMapKeyRef env var to the container.
func WithConfigMapKeyRef(cmName, key, envVarName string) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Template.Spec.Containers[0].Env = append(
d.Spec.Template.Spec.Containers[0].Env,
corev1.EnvVar{
Name: envVarName,
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
Key: key,
},
},
},
)
}
}
// WithSecretKeyRef adds a valueFrom.secretKeyRef env var to the container.
func WithSecretKeyRef(secretName, key, envVarName string) DeploymentOption {
return func(d *appsv1.Deployment) {
d.Spec.Template.Spec.Containers[0].Env = append(
d.Spec.Template.Spec.Containers[0].Env,
corev1.EnvVar{
Name: envVarName,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
Key: key,
},
},
},
)
}
}
// WithPodTemplateAnnotations adds annotations to the pod template metadata (not deployment metadata).
func WithPodTemplateAnnotations(annotations map[string]string) DeploymentOption {
return func(d *appsv1.Deployment) {
if d.Spec.Template.Annotations == nil {
d.Spec.Template.Annotations = make(map[string]string)
}
for k, v := range annotations {
d.Spec.Template.Annotations[k] = v
}
}
}
// WithInitContainerVolume adds an init container with ConfigMap/Secret volume mounts.
func WithInitContainerVolume(cmName, secretName string) DeploymentOption {
return func(d *appsv1.Deployment) {
initContainer := corev1.Container{
Name: "init",
Image: DefaultImage,
Command: []string{"sh", "-c", "echo init done"},
}
if cmName != "" {
volumeName := fmt.Sprintf("init-cm-%s", cmName)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
},
},
})
initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{
Name: volumeName,
MountPath: fmt.Sprintf("/etc/init-config/%s", cmName),
})
}
if secretName != "" {
volumeName := fmt.Sprintf("init-secret-%s", secretName)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
},
},
})
initContainer.VolumeMounts = append(initContainer.VolumeMounts, corev1.VolumeMount{
Name: volumeName,
MountPath: fmt.Sprintf("/etc/init-secrets/%s", secretName),
})
}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, initContainer)
}
}
// WithInitContainerProjectedVolume adds an init container with projected volume.
func WithInitContainerProjectedVolume(cmName, secretName string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := "init-projected-config"
sources := []corev1.VolumeProjection{}
if cmName != "" {
sources = append(sources, corev1.VolumeProjection{
ConfigMap: &corev1.ConfigMapProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
},
})
}
if secretName != "" {
sources = append(sources, corev1.VolumeProjection{
Secret: &corev1.SecretProjection{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
},
})
}
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
Projected: &corev1.ProjectedVolumeSource{
Sources: sources,
},
},
})
initContainer := corev1.Container{
Name: "init",
Image: DefaultImage,
Command: []string{"sh", "-c", "echo init done"},
VolumeMounts: []corev1.VolumeMount{
{
Name: volumeName,
MountPath: "/etc/init-projected",
},
},
}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, initContainer)
}
}
// WithCSIVolume adds a CSI volume referencing a SecretProviderClass to a Deployment.
func WithCSIVolume(spcName string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := csiVolumeName(spcName)
mountPath := csiMountPath(spcName)
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: CSIDriverName,
ReadOnly: ptr.To(true),
VolumeAttributes: map[string]string{
"secretProviderClass": spcName,
},
},
},
})
d.Spec.Template.Spec.Containers[0].VolumeMounts = append(
d.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: mountPath,
ReadOnly: true,
},
)
}
}
// WithInitContainerCSIVolume adds an init container with a CSI volume mount.
func WithInitContainerCSIVolume(spcName string) DeploymentOption {
return func(d *appsv1.Deployment) {
volumeName := csiVolumeName(spcName)
mountPath := csiMountPath(spcName)
hasCSIVolume := false
for _, v := range d.Spec.Template.Spec.Volumes {
if v.Name == volumeName {
hasCSIVolume = true
break
}
}
if !hasCSIVolume {
d.Spec.Template.Spec.Volumes = append(d.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: CSIDriverName,
ReadOnly: ptr.To(true),
VolumeAttributes: map[string]string{
"secretProviderClass": spcName,
},
},
},
})
}
initContainer := corev1.Container{
Name: fmt.Sprintf("init-csi-%s", spcName),
Image: DefaultImage,
Command: []string{"sh", "-c", "echo init done"},
VolumeMounts: []corev1.VolumeMount{
{
Name: volumeName,
MountPath: mountPath,
ReadOnly: true,
},
},
}
d.Spec.Template.Spec.InitContainers = append(d.Spec.Template.Spec.InitContainers, initContainer)
}
}
func baseDeploymentResource(namespace, name string) *appsv1.Deployment {
labels := map[string]string{"app": name}
return &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: ptr.To(int32(1)),
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: DefaultImage,
Command: []string{"sh", "-c", DefaultCommand},
},
},
},
},
},
}
}
// DeleteDeployment deletes a Deployment.
func DeleteDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.AppsV1().Deployments(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// DaemonSetOption is a functional option for configuring a DaemonSet.
type DaemonSetOption func(*appsv1.DaemonSet)
// CreateDaemonSet creates a DaemonSet with the given options.
func CreateDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...DaemonSetOption) (*appsv1.DaemonSet, error) {
ds := baseDaemonSetResource(namespace, name)
for _, opt := range opts {
opt(ds)
}
return client.AppsV1().DaemonSets(namespace).Create(ctx, ds, metav1.CreateOptions{})
}
// baseDaemonSetResource creates a base DaemonSet template.
func baseDaemonSetResource(namespace, name string) *appsv1.DaemonSet {
labels := map[string]string{"app": name}
return &appsv1.DaemonSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.DaemonSetSpec{
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: DefaultImage,
Command: []string{"sh", "-c", DefaultCommand},
},
},
},
},
},
}
}
// DeleteDaemonSet deletes a DaemonSet.
func DeleteDaemonSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.AppsV1().DaemonSets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// StatefulSetOption is a functional option for configuring a StatefulSet.
type StatefulSetOption func(*appsv1.StatefulSet)
// CreateStatefulSet creates a StatefulSet with the given options.
func CreateStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...StatefulSetOption) (*appsv1.StatefulSet, error) {
ss := baseStatefulSetResource(namespace, name)
for _, opt := range opts {
opt(ss)
}
return client.AppsV1().StatefulSets(namespace).Create(ctx, ss, metav1.CreateOptions{})
}
// baseStatefulSetResource creates a base StatefulSet template.
func baseStatefulSetResource(namespace, name string) *appsv1.StatefulSet {
labels := map[string]string{"app": name}
return &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: appsv1.StatefulSetSpec{
ServiceName: name,
Replicas: ptr.To(int32(1)),
Selector: &metav1.LabelSelector{
MatchLabels: labels,
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{
{
Name: "app",
Image: DefaultImage,
Command: []string{"sh", "-c", DefaultCommand},
},
},
},
},
},
}
}
// DeleteStatefulSet deletes a StatefulSet.
func DeleteStatefulSet(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.AppsV1().StatefulSets(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// CronJobOption is a functional option for configuring a CronJob.
type CronJobOption func(*batchv1.CronJob)
// CreateCronJob creates a CronJob with the given options.
func CreateCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...CronJobOption) (*batchv1.CronJob, error) {
cj := baseCronJobResource(namespace, name)
for _, opt := range opts {
opt(cj)
}
return client.BatchV1().CronJobs(namespace).Create(ctx, cj, metav1.CreateOptions{})
}
// WithCronJobAnnotations adds annotations to the CronJob metadata.
func WithCronJobAnnotations(annotations map[string]string) CronJobOption {
return func(cj *batchv1.CronJob) {
if cj.Annotations == nil {
cj.Annotations = make(map[string]string)
}
for k, v := range annotations {
cj.Annotations[k] = v
}
}
}
// WithCronJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap.
func WithCronJobConfigMapEnvFrom(name string) CronJobOption {
return func(cj *batchv1.CronJob) {
cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append(
cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// WithCronJobSecretEnvFrom adds an envFrom reference to a Secret.
func WithCronJobSecretEnvFrom(name string) CronJobOption {
return func(cj *batchv1.CronJob) {
cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom = append(
cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// baseCronJobResource creates a base CronJob template.
func baseCronJobResource(namespace, name string) *batchv1.CronJob {
labels := map[string]string{"app": name}
return &batchv1.CronJob{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: batchv1.CronJobSpec{
Schedule: "* * * * *", // Every minute
JobTemplate: batchv1.JobTemplateSpec{
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyOnFailure,
Containers: []corev1.Container{
{
Name: "job",
Image: DefaultImage,
Command: []string{"sh", "-c", "echo done"},
},
},
},
},
},
},
},
}
}
// DeleteCronJob deletes a CronJob.
func DeleteCronJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
return client.BatchV1().CronJobs(namespace).Delete(ctx, name, metav1.DeleteOptions{})
}
// JobOption is a functional option for configuring a Job.
type JobOption func(*batchv1.Job)
// CreateJob creates a Job with the given options.
func CreateJob(ctx context.Context, client kubernetes.Interface, namespace, name string, opts ...JobOption) (*batchv1.Job, error) {
job := baseJobResource(namespace, name)
for _, opt := range opts {
opt(job)
}
return client.BatchV1().Jobs(namespace).Create(ctx, job, metav1.CreateOptions{})
}
// WithJobAnnotations adds annotations to the Job metadata.
func WithJobAnnotations(annotations map[string]string) JobOption {
return func(j *batchv1.Job) {
if j.Annotations == nil {
j.Annotations = make(map[string]string)
}
for k, v := range annotations {
j.Annotations[k] = v
}
}
}
// WithJobConfigMapEnvFrom adds an envFrom reference to a ConfigMap.
func WithJobConfigMapEnvFrom(name string) JobOption {
return func(j *batchv1.Job) {
j.Spec.Template.Spec.Containers[0].EnvFrom = append(
j.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
ConfigMapRef: &corev1.ConfigMapEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// WithJobSecretEnvFrom adds an envFrom reference to a Secret.
func WithJobSecretEnvFrom(name string) JobOption {
return func(j *batchv1.Job) {
j.Spec.Template.Spec.Containers[0].EnvFrom = append(
j.Spec.Template.Spec.Containers[0].EnvFrom,
corev1.EnvFromSource{
SecretRef: &corev1.SecretEnvSource{
LocalObjectReference: corev1.LocalObjectReference{Name: name},
},
},
)
}
}
// WithJobConfigMapKeyRef adds a valueFrom.configMapKeyRef env var to a Job.
func WithJobConfigMapKeyRef(cmName, key, envVarName string) JobOption {
return func(j *batchv1.Job) {
j.Spec.Template.Spec.Containers[0].Env = append(
j.Spec.Template.Spec.Containers[0].Env,
corev1.EnvVar{
Name: envVarName,
ValueFrom: &corev1.EnvVarSource{
ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: cmName},
Key: key,
},
},
},
)
}
}
// WithJobSecretKeyRef adds a valueFrom.secretKeyRef env var to a Job.
func WithJobSecretKeyRef(secretName, key, envVarName string) JobOption {
return func(j *batchv1.Job) {
j.Spec.Template.Spec.Containers[0].Env = append(
j.Spec.Template.Spec.Containers[0].Env,
corev1.EnvVar{
Name: envVarName,
ValueFrom: &corev1.EnvVarSource{
SecretKeyRef: &corev1.SecretKeySelector{
LocalObjectReference: corev1.LocalObjectReference{Name: secretName},
Key: key,
},
},
},
)
}
}
// WithJobCSIVolume adds a CSI volume referencing a SecretProviderClass to a Job.
func WithJobCSIVolume(spcName string) JobOption {
return func(j *batchv1.Job) {
volumeName := csiVolumeName(spcName)
mountPath := csiMountPath(spcName)
j.Spec.Template.Spec.Volumes = append(j.Spec.Template.Spec.Volumes, corev1.Volume{
Name: volumeName,
VolumeSource: corev1.VolumeSource{
CSI: &corev1.CSIVolumeSource{
Driver: CSIDriverName,
ReadOnly: ptr.To(true),
VolumeAttributes: map[string]string{
"secretProviderClass": spcName,
},
},
},
})
j.Spec.Template.Spec.Containers[0].VolumeMounts = append(
j.Spec.Template.Spec.Containers[0].VolumeMounts,
corev1.VolumeMount{
Name: volumeName,
MountPath: mountPath,
ReadOnly: true,
},
)
}
}
// baseJobResource creates a base Job template.
func baseJobResource(namespace, name string) *batchv1.Job {
labels := map[string]string{"app": name}
return &batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: batchv1.JobSpec{
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
},
Spec: corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
Containers: []corev1.Container{
{
Name: "job",
Image: DefaultImage,
Command: []string{"sh", "-c", "echo done"},
},
},
},
},
},
}
}
// DeleteJob deletes a Job.
func DeleteJob(ctx context.Context, client kubernetes.Interface, namespace, name string) error {
propagation := metav1.DeletePropagationBackground
return client.BatchV1().Jobs(namespace).Delete(ctx, name, metav1.DeleteOptions{
PropagationPolicy: &propagation,
})
}
func csiVolumeName(spcName string) string {
return fmt.Sprintf("csi-%s", spcName)
}
func csiMountPath(spcName string) string {
return fmt.Sprintf("/mnt/secrets-store/%s", spcName)
}
// GetDeployment retrieves a deployment by name.
func GetDeployment(ctx context.Context, client kubernetes.Interface, namespace, name string) (*appsv1.Deployment, error) {
return client.AppsV1().Deployments(namespace).Get(ctx, name, metav1.GetOptions{})
}
// GetPodLogs retrieves logs from pods matching the given label selector.
func GetPodLogs(ctx context.Context, client kubernetes.Interface, namespace, labelSelector string) (string, error) {
pods, err := client.CoreV1().Pods(namespace).List(
ctx, metav1.ListOptions{
LabelSelector: labelSelector,
},
)
if err != nil {
return "", fmt.Errorf("failed to list pods: %w", err)
}
var allLogs strings.Builder
for _, pod := range pods.Items {
for _, container := range pod.Spec.Containers {
logs, err := client.CoreV1().Pods(namespace).GetLogs(
pod.Name, &corev1.PodLogOptions{
Container: container.Name,
},
).Do(ctx).Raw()
if err != nil {
allLogs.WriteString(fmt.Sprintf("Error getting logs for %s/%s: %v\n", pod.Name, container.Name, err))
continue
}
allLogs.WriteString(fmt.Sprintf("=== %s/%s ===\n%s\n", pod.Name, container.Name, string(logs)))
}
}
return allLogs.String(), nil
}