diff --git a/.wordlist.txt b/.wordlist.txt index 2f436ef..a87ab7b 100644 --- a/.wordlist.txt +++ b/.wordlist.txt @@ -1,3 +1,4 @@ +AdditionalContainerArgs Akamai Azurite BarmanObjectStore diff --git a/api/v1/objectstore_types.go b/api/v1/objectstore_types.go index 0db706a..d9ceb52 100644 --- a/api/v1/objectstore_types.go +++ b/api/v1/objectstore_types.go @@ -37,6 +37,12 @@ type InstanceSidecarConfiguration struct { // Resources define cpu/memory requests and limits for the sidecar that runs in the instance pods. // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` + + // AdditionalContainerArgs is an optional list of command-line arguments + // to be passed to the sidecar container when it starts. + // The provided arguments are appended to the container’s default arguments. + // +optional + AdditionalContainerArgs []string `json:"additionalContainerArgs,omitempty"` } // ObjectStoreSpec defines the desired state of ObjectStore. diff --git a/api/v1/zz_generated.deepcopy.go b/api/v1/zz_generated.deepcopy.go index 1f92d88..a86696b 100644 --- a/api/v1/zz_generated.deepcopy.go +++ b/api/v1/zz_generated.deepcopy.go @@ -36,6 +36,11 @@ func (in *InstanceSidecarConfiguration) DeepCopyInto(out *InstanceSidecarConfigu } } in.Resources.DeepCopyInto(&out.Resources) + if in.AdditionalContainerArgs != nil { + in, out := &in.AdditionalContainerArgs, &out.AdditionalContainerArgs + *out = make([]string, len(*in)) + copy(*out, *in) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InstanceSidecarConfiguration. diff --git a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml index be1348d..0f733f0 100644 --- a/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml +++ b/config/crd/bases/barmancloud.cnpg.io_objectstores.yaml @@ -391,6 +391,14 @@ spec: description: The configuration for the sidecar that runs in the instance pods properties: + additionalContainerArgs: + description: |- + AdditionalContainerArgs is an optional list of command-line arguments + to be passed to the sidecar container when it starts. + The provided arguments are appended to the container’s default arguments. + items: + type: string + type: array env: description: The environment to be explicitly passed to the sidecar items: diff --git a/hack/examples/minio-store.yaml b/hack/examples/minio-store.yaml index de47ea7..26ed965 100644 --- a/hack/examples/minio-store.yaml +++ b/hack/examples/minio-store.yaml @@ -13,6 +13,8 @@ spec: limits: memory: "512Mi" cpu: "500m" + additionalContainerArgs: + - --log-level=debug configuration: endpointCA: name: minio-server-tls diff --git a/internal/cnpgi/operator/lifecycle.go b/internal/cnpgi/operator/lifecycle.go index d5c9918..d7eba1f 100644 --- a/internal/cnpgi/operator/lifecycle.go +++ b/internal/cnpgi/operator/lifecycle.go @@ -17,6 +17,7 @@ import ( "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" ) @@ -133,9 +134,10 @@ func (impl LifecycleImplementation) reconcileJob( } type sidecarConfiguration struct { - env []corev1.EnvVar - certificates []corev1.VolumeProjection - resources corev1.ResourceRequirements + env []corev1.EnvVar + certificates []corev1.VolumeProjection + resources corev1.ResourceRequirements + additionalArgs []string } func reconcileJob( @@ -217,14 +219,47 @@ func (impl LifecycleImplementation) reconcilePod( return nil, err } - return reconcilePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{ - env: env, - certificates: certificates, - resources: resources, + additionalArgs, err := impl.collectAdditionalInstanceArgs(ctx, pluginConfiguration) + if err != nil { + return nil, err + } + + return reconcileInstancePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{ + env: env, + certificates: certificates, + resources: resources, + additionalArgs: additionalArgs, }) } -func reconcilePod( +func (impl LifecycleImplementation) collectAdditionalInstanceArgs( + ctx context.Context, + pluginConfiguration *config.PluginConfiguration, +) ([]string, error) { + // Prefer the cluster object store (backup/archive). If not set, fallback to the recovery object store. + // If neither is configured, no additional args are provided. + if len(pluginConfiguration.BarmanObjectName) > 0 { + var barmanObjectStore barmancloudv1.ObjectStore + if err := impl.Client.Get(ctx, pluginConfiguration.GetBarmanObjectKey(), &barmanObjectStore); err != nil { + return nil, fmt.Errorf("while getting barman object store %s: %w", + pluginConfiguration.GetBarmanObjectKey().String(), err) + } + return barmanObjectStore.Spec.InstanceSidecarConfiguration.AdditionalContainerArgs, nil + } + + if len(pluginConfiguration.RecoveryBarmanObjectName) > 0 { + var barmanObjectStore barmancloudv1.ObjectStore + if err := impl.Client.Get(ctx, pluginConfiguration.GetRecoveryBarmanObjectKey(), &barmanObjectStore); err != nil { + return nil, fmt.Errorf("while getting recovery barman object store %s: %w", + pluginConfiguration.GetRecoveryBarmanObjectKey().String(), err) + } + return barmanObjectStore.Spec.InstanceSidecarConfiguration.AdditionalContainerArgs, nil + } + + return nil, nil +} + +func reconcileInstancePod( ctx context.Context, cluster *cnpgv1.Cluster, request *lifecycle.OperatorLifecycleRequest, @@ -332,6 +367,7 @@ func reconcilePodSpec( } sidecarTemplate.RestartPolicy = ptr.To(corev1.ContainerRestartPolicyAlways) sidecarTemplate.Resources = config.resources + sidecarTemplate.Args = append(sidecarTemplate.Args, config.additionalArgs...) // merge the main container envs if they aren't already set for _, container := range spec.Containers { diff --git a/internal/cnpgi/operator/lifecycle_test.go b/internal/cnpgi/operator/lifecycle_test.go index c3235de..675d657 100644 --- a/internal/cnpgi/operator/lifecycle_test.go +++ b/internal/cnpgi/operator/lifecycle_test.go @@ -6,9 +6,12 @@ import ( cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1" "github.com/cloudnative-pg/cloudnative-pg/pkg/utils" "github.com/cloudnative-pg/cnpg-i/pkg/lifecycle" + barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/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/runtime" + "sigs.k8s.io/controller-runtime/pkg/client/fake" "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config" @@ -18,7 +21,6 @@ import ( var _ = Describe("LifecycleImplementation", func() { var ( - lifecycleImpl LifecycleImplementation pluginConfiguration *config.PluginConfiguration cluster *cnpgv1.Cluster jobTypeMeta = metav1.TypeMeta{ @@ -31,6 +33,26 @@ var _ = Describe("LifecycleImplementation", func() { } ) + // helper to build a fake client with our scheme and optional objects + buildClientFunc := func(objs ...runtime.Object) *fake.ClientBuilder { + s := runtime.NewScheme() + _ = barmancloudv1.AddToScheme(s) + return fake.NewClientBuilder().WithScheme(s).WithRuntimeObjects(objs...) + } + + // helper to create an ObjectStore with given args + makeStoreFunc := func(ns, name string, args []string) *barmancloudv1.ObjectStore { + return &barmancloudv1.ObjectStore{ + TypeMeta: metav1.TypeMeta{Kind: "ObjectStore", APIVersion: barmancloudv1.GroupVersion.String()}, + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: ns}, + Spec: barmancloudv1.ObjectStoreSpec{ + InstanceSidecarConfiguration: barmancloudv1.InstanceSidecarConfiguration{ + AdditionalContainerArgs: args, + }, + }, + } + } + BeforeEach(func() { pluginConfiguration = &config.PluginConfiguration{ BarmanObjectName: "minio-store-dest", @@ -67,6 +89,7 @@ var _ = Describe("LifecycleImplementation", func() { Describe("GetCapabilities", func() { It("returns the correct capabilities", func(ctx SpecContext) { + var lifecycleImpl LifecycleImplementation response, err := lifecycleImpl.GetCapabilities(ctx, &lifecycle.OperatorLifecycleCapabilitiesRequest{}) Expect(err).NotTo(HaveOccurred()) Expect(response).NotTo(BeNil()) @@ -76,6 +99,7 @@ var _ = Describe("LifecycleImplementation", func() { Describe("LifecycleHook", func() { It("returns an error if object definition is invalid", func(ctx SpecContext) { + var lifecycleImpl LifecycleImplementation request := &lifecycle.OperatorLifecycleRequest{ ObjectDefinition: []byte("invalid-json"), } @@ -171,7 +195,7 @@ var _ = Describe("LifecycleImplementation", func() { }) }) - Describe("reconcilePod", func() { + Describe("reconcileInstancePod", func() { It("returns a patch for a valid pod", func(ctx SpecContext) { pod := &corev1.Pod{ TypeMeta: podTypeMeta, @@ -185,7 +209,7 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: podJSON, } - response, err := reconcilePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{}) + response, err := reconcileInstancePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{}) Expect(err).NotTo(HaveOccurred()) Expect(response).NotTo(BeNil()) Expect(response.JsonPatch).NotTo(BeEmpty()) @@ -203,11 +227,93 @@ var _ = Describe("LifecycleImplementation", func() { ObjectDefinition: []byte("invalid-json"), } - response, err := reconcilePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{}) + response, err := reconcileInstancePod(ctx, cluster, request, pluginConfiguration, sidecarConfiguration{}) Expect(err).To(HaveOccurred()) Expect(response).To(BeNil()) }) }) + + Describe("collectAdditionalInstanceArgs", func() { + It("prefers cluster object store when both are configured", func(ctx SpecContext) { + ns := "test-ns" + cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}} + pc := &config.PluginConfiguration{ + Cluster: cluster, + BarmanObjectName: "primary-store", + RecoveryBarmanObjectName: "recovery-store", + } + primaryArgs := []string{"--primary-a", "--primary-b"} + recoveryArgs := []string{"--reco-a"} + cli := buildClientFunc( + makeStoreFunc(ns, pc.BarmanObjectName, primaryArgs), + makeStoreFunc(ns, pc.RecoveryBarmanObjectName, recoveryArgs), + ).Build() + + impl := LifecycleImplementation{Client: cli} + args, err := impl.collectAdditionalInstanceArgs(ctx, pc) + Expect(err).NotTo(HaveOccurred()) + Expect(args).To(Equal(primaryArgs)) + }) + + It("falls back to recovery object store when primary not set", func(ctx SpecContext) { + ns := "test-ns" + cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}} + pc := &config.PluginConfiguration{ + Cluster: cluster, + BarmanObjectName: "", + RecoveryBarmanObjectName: "recovery-store", + } + recoveryArgs := []string{"--reco-x", "--reco-y"} + cli := buildClientFunc( + makeStoreFunc(ns, pc.RecoveryBarmanObjectName, recoveryArgs), + ).Build() + + impl := LifecycleImplementation{Client: cli} + args, err := impl.collectAdditionalInstanceArgs(ctx, pc) + Expect(err).NotTo(HaveOccurred()) + Expect(args).To(Equal(recoveryArgs)) + }) + + It("returns nil when neither object name is configured", func(ctx SpecContext) { + ns := "test-ns" + cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}} + pc := &config.PluginConfiguration{Cluster: cluster} + cli := buildClientFunc().Build() + + impl := LifecycleImplementation{Client: cli} + args, err := impl.collectAdditionalInstanceArgs(ctx, pc) + Expect(err).NotTo(HaveOccurred()) + Expect(args).To(BeNil()) + }) + + It("returns error if primary object store cannot be retrieved", func(ctx SpecContext) { + ns := "test-ns" + cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}} + pc := &config.PluginConfiguration{Cluster: cluster, BarmanObjectName: "missing-store"} + cli := buildClientFunc().Build() + + impl := LifecycleImplementation{Client: cli} + args, err := impl.collectAdditionalInstanceArgs(ctx, pc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("while getting barman object store")) + Expect(err.Error()).To(ContainSubstring(ns + "/" + pc.BarmanObjectName)) + Expect(args).To(BeNil()) + }) + + It("returns error if recovery object store cannot be retrieved", func(ctx SpecContext) { + ns := "test-ns" + cluster := &cnpgv1.Cluster{ObjectMeta: metav1.ObjectMeta{Name: "c", Namespace: ns}} + pc := &config.PluginConfiguration{Cluster: cluster, RecoveryBarmanObjectName: "missing-reco"} + cli := buildClientFunc().Build() + + impl := LifecycleImplementation{Client: cli} + args, err := impl.collectAdditionalInstanceArgs(ctx, pc) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("while getting recovery barman object store")) + Expect(err.Error()).To(ContainSubstring(ns + "/" + pc.RecoveryBarmanObjectName)) + Expect(args).To(BeNil()) + }) + }) }) var _ = Describe("Volume utilities", func() { diff --git a/manifest.yaml b/manifest.yaml index 7248ce9..fb157ea 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -390,6 +390,14 @@ spec: description: The configuration for the sidecar that runs in the instance pods properties: + additionalContainerArgs: + description: |- + AdditionalContainerArgs is an optional list of command-line arguments + to be passed to the sidecar container when it starts. + The provided arguments are appended to the container’s default arguments. + items: + type: string + type: array env: description: The environment to be explicitly passed to the sidecar items: diff --git a/web/docs/plugin-barman-cloud.v1.md b/web/docs/plugin-barman-cloud.v1.md index 5723283..ea50bd9 100644 --- a/web/docs/plugin-barman-cloud.v1.md +++ b/web/docs/plugin-barman-cloud.v1.md @@ -29,6 +29,7 @@ _Appears in:_ | `env` _[EnvVar](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#envvar-v1-core) array_ | The environment to be explicitly passed to the sidecar | | | | | `retentionPolicyIntervalSeconds` _integer_ | The retentionCheckInterval defines the frequency at which the
system checks and enforces retention policies. | | 1800 | | | `resources` _[ResourceRequirements](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.32/#resourcerequirements-v1-core)_ | Resources define cpu/memory requests and limits for the sidecar that runs in the instance pods. | | | | +| `additionalContainerArgs` _string array_ | AdditionalContainerArgs is an optional list of command-line arguments
to be passed to the sidecar container when it starts.
The provided arguments are appended to the container’s default arguments. | | | | #### ObjectStore