From be4037529c44858278dd80e3eb32f39f3f68c5c6 Mon Sep 17 00:00:00 2001 From: Leonardo Cecchi Date: Thu, 5 Dec 2024 13:18:18 +0100 Subject: [PATCH] feat: additional environment variables (#81) Signed-off-by: Leonardo Cecchi Signed-off-by: Francesco Canovai Co-authored-by: Francesco Canovai --- api/v1/objectstore_types.go | 5 + .../barmancloud.cnpg.io_objectstores.yaml | 123 ++++++++++++++++++ internal/cnpgi/operator/lifecycle.go | 105 ++++++++++++++- internal/cnpgi/operator/lifecycle_test.go | 12 +- internal/cnpgi/operator/start.go | 4 +- 5 files changed, 237 insertions(+), 12 deletions(-) diff --git a/api/v1/objectstore_types.go b/api/v1/objectstore_types.go index 70c2a52..57a5d76 100644 --- a/api/v1/objectstore_types.go +++ b/api/v1/objectstore_types.go @@ -18,6 +18,7 @@ package v1 import ( barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -29,6 +30,10 @@ type InstanceSidecarConfiguration struct { // +kubebuilder:validation:Maximum=3600 // +kubebuilder:default=180 CacheTTL *int `json:"cacheTTL,omitempty"` + + // The environment to be explicitly passed to the sidecar + // +optional + Env []corev1.EnvVar `json:"env,omitempty"` } // GetCacheTTL returns the cache TTL value, defaulting to 180 seconds if not set. diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index 3230ef8..4f087b8 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -389,6 +389,126 @@ spec: maximum: 3600 minimum: 0 type: integer + env: + description: The environment to be explicitly passed to the sidecar + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: Name of the environment variable. Must be a + C_IDENTIFIER. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array type: object required: - configuration @@ -396,6 +516,9 @@ spec: status: description: ObjectStoreStatus defines the observed state of ObjectStore. type: object + required: + - metadata + - spec type: object served: true storage: true diff --git a/internal/cnpgi/operator/lifecycle.go b/internal/cnpgi/operator/lifecycle.go index 841ec3c..61a1e5e 100644 --- a/internal/cnpgi/operator/lifecycle.go +++ b/internal/cnpgi/operator/lifecycle.go @@ -14,8 +14,11 @@ import ( "github.com/spf13/viper" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" "k8s.io/utils/ptr" + "sigs.k8s.io/controller-runtime/pkg/client" + barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config" ) @@ -23,6 +26,7 @@ import ( // LifecycleImplementation is the implementation of the lifecycle handler type LifecycleImplementation struct { lifecycle.UnimplementedOperatorLifecycleServer + Client client.Client } // GetCapabilities exposes the lifecycle capabilities @@ -94,20 +98,85 @@ func (impl LifecycleImplementation) LifecycleHook( switch kind { case "Pod": contextLogger.Info("Reconciling pod") - return reconcilePod(ctx, &cluster, request, pluginConfiguration) + return impl.reconcilePod(ctx, &cluster, request, pluginConfiguration) case "Job": contextLogger.Info("Reconciling job") - return reconcileJob(ctx, &cluster, request, pluginConfiguration) + return impl.reconcileJob(ctx, &cluster, request, pluginConfiguration) default: return nil, fmt.Errorf("unsupported kind: %s", kind) } } +func (impl LifecycleImplementation) collectAdditionalEnvs( + ctx context.Context, + namespace string, + pluginConfiguration *config.PluginConfiguration, +) ([]corev1.EnvVar, error) { + var result []corev1.EnvVar + + if len(pluginConfiguration.BarmanObjectName) > 0 { + envs, err := impl.collectObjectStoreEnvs( + ctx, + types.NamespacedName{ + Name: pluginConfiguration.BarmanObjectName, + Namespace: namespace, + }, + ) + if err != nil { + return nil, err + } + result = append(result, envs...) + } + + if len(pluginConfiguration.RecoveryBarmanObjectName) > 0 { + envs, err := impl.collectObjectStoreEnvs( + ctx, + types.NamespacedName{ + Name: pluginConfiguration.RecoveryBarmanObjectName, + Namespace: namespace, + }, + ) + if err != nil { + return nil, err + } + result = append(result, envs...) + } + + return result, nil +} + +func (impl LifecycleImplementation) collectObjectStoreEnvs( + ctx context.Context, + barmanObjectKey types.NamespacedName, +) ([]corev1.EnvVar, error) { + var objectStore barmancloudv1.ObjectStore + if err := impl.Client.Get(ctx, barmanObjectKey, &objectStore); err != nil { + return nil, err + } + + return objectStore.Spec.InstanceSidecarConfiguration.Env, nil +} + +func (impl LifecycleImplementation) reconcileJob( + ctx context.Context, + cluster *cnpgv1.Cluster, + request *lifecycle.OperatorLifecycleRequest, + pluginConfiguration *config.PluginConfiguration, +) (*lifecycle.OperatorLifecycleResponse, error) { + env, err := impl.collectAdditionalEnvs(ctx, cluster.Namespace, pluginConfiguration) + if err != nil { + return nil, nil + } + + return reconcileJob(ctx, cluster, request, pluginConfiguration, env) +} + func reconcileJob( ctx context.Context, cluster *cnpgv1.Cluster, request *lifecycle.OperatorLifecycleRequest, pluginConfiguration *config.PluginConfiguration, + env []corev1.EnvVar, ) (*lifecycle.OperatorLifecycleResponse, error) { contextLogger := log.FromContext(ctx).WithName("lifecycle") if pluginConfig := cluster.GetRecoverySourcePlugin(); pluginConfig == nil || pluginConfig.Name != metadata.PluginName { @@ -144,6 +213,7 @@ func reconcileJob( corev1.Container{ Args: []string{"restore"}, }, + env, ); err != nil { return nil, fmt.Errorf("while reconciling pod spec for job: %w", err) } @@ -159,11 +229,26 @@ func reconcileJob( }, nil } +func (impl LifecycleImplementation) reconcilePod( + ctx context.Context, + cluster *cnpgv1.Cluster, + request *lifecycle.OperatorLifecycleRequest, + pluginConfiguration *config.PluginConfiguration, +) (*lifecycle.OperatorLifecycleResponse, error) { + env, err := impl.collectAdditionalEnvs(ctx, cluster.Namespace, pluginConfiguration) + if err != nil { + return nil, nil + } + + return reconcilePod(ctx, cluster, request, pluginConfiguration, env) +} + func reconcilePod( ctx context.Context, cluster *cnpgv1.Cluster, request *lifecycle.OperatorLifecycleRequest, pluginConfiguration *config.PluginConfiguration, + env []corev1.EnvVar, ) (*lifecycle.OperatorLifecycleResponse, error) { pod, err := decoder.DecodePodJSON(request.GetObjectDefinition()) if err != nil { @@ -176,9 +261,16 @@ func reconcilePod( mutatedPod := pod.DeepCopy() if len(pluginConfiguration.BarmanObjectName) != 0 { - if err := reconcilePodSpec(pluginConfiguration, cluster, &mutatedPod.Spec, "postgres", corev1.Container{ - Args: []string{"instance"}, - }); err != nil { + if err := reconcilePodSpec( + pluginConfiguration, + cluster, + &mutatedPod.Spec, + "postgres", + corev1.Container{ + Args: []string{"instance"}, + }, + env, + ); err != nil { return nil, fmt.Errorf("while reconciling pod spec for pod: %w", err) } } else { @@ -202,6 +294,7 @@ func reconcilePodSpec( spec *corev1.PodSpec, mainContainerName string, sidecarConfig corev1.Container, + additionalEnvs []corev1.EnvVar, ) error { envs := []corev1.EnvVar{ { @@ -246,6 +339,8 @@ func reconcilePodSpec( ) } + envs = append(envs, additionalEnvs...) + baseProbe := &corev1.Probe{ FailureThreshold: 3, ProbeHandler: corev1.ProbeHandler{ diff --git a/internal/cnpgi/operator/lifecycle_test.go b/internal/cnpgi/operator/lifecycle_test.go index 2b59e40..29285eb 100644 --- a/internal/cnpgi/operator/lifecycle_test.go +++ b/internal/cnpgi/operator/lifecycle_test.go @@ -107,7 +107,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: jobJSON, } - response, err := reconcileJob(ctx, cluster, request, pluginConfiguration) + response, err := reconcileJob(ctx, cluster, request, pluginConfiguration, nil) Expect(err).NotTo(HaveOccurred()) Expect(response).NotTo(BeNil()) Expect(response.JsonPatch).NotTo(BeEmpty()) @@ -128,7 +128,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: jobJSON, } - response, err := reconcileJob(ctx, cluster, request, pluginConfiguration) + response, err := reconcileJob(ctx, cluster, request, pluginConfiguration, nil) Expect(err).NotTo(HaveOccurred()) Expect(response).To(BeNil()) }) @@ -138,7 +138,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: []byte("invalid-json"), } - response, err := reconcileJob(ctx, cluster, request, pluginConfiguration) + response, err := reconcileJob(ctx, cluster, request, pluginConfiguration, nil) Expect(err).To(HaveOccurred()) Expect(response).To(BeNil()) }) @@ -165,7 +165,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: jobJSON, } - response, err := reconcileJob(ctx, cluster, request, pluginConfiguration) + response, err := reconcileJob(ctx, cluster, request, pluginConfiguration, nil) Expect(err).NotTo(HaveOccurred()) Expect(response).To(BeNil()) }) @@ -185,7 +185,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: podJSON, } - response, err := reconcilePod(ctx, cluster, request, pluginConfiguration) + response, err := reconcilePod(ctx, cluster, request, pluginConfiguration, nil) Expect(err).NotTo(HaveOccurred()) Expect(response).NotTo(BeNil()) Expect(response.JsonPatch).NotTo(BeEmpty()) @@ -203,7 +203,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: []byte("invalid-json"), } - response, err := reconcilePod(ctx, cluster, request, pluginConfiguration) + response, err := reconcilePod(ctx, cluster, request, pluginConfiguration, nil) Expect(err).To(HaveOccurred()) Expect(response).To(BeNil()) }) diff --git a/internal/cnpgi/operator/start.go b/internal/cnpgi/operator/start.go index 3c8eff6..f7c59be 100644 --- a/internal/cnpgi/operator/start.go +++ b/internal/cnpgi/operator/start.go @@ -27,7 +27,9 @@ func (c *CNPGI) Start(ctx context.Context) error { reconciler.RegisterReconcilerHooksServer(server, ReconcilerImplementation{ Client: c.Client, }) - lifecycle.RegisterOperatorLifecycleServer(server, LifecycleImplementation{}) + lifecycle.RegisterOperatorLifecycleServer(server, LifecycleImplementation{ + Client: c.Client, + }) return nil }