mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-07-06 17:52:21 +02:00
fix(rbac): reconcile Role when ObjectStore spec changes
When an ObjectStore's credentials change (e.g., secret rename), the RBAC Role granting the Cluster's ServiceAccount access to those secrets was not updated because nothing triggered a Cluster reconciliation. Implement the ObjectStore controller's Reconcile to detect referencing Clusters and update their Roles directly. Extract ensureRole into a shared rbac.EnsureRole function used by both the Pre hook and the ObjectStore controller. Handle concurrent modifications between the Pre hook and ObjectStore controller gracefully: AlreadyExists on Create and Conflict on Patch are retried once to avoid propagating transient errors as gRPC failures to CNPG. Replace the custom setOwnerReference helper (ownership.go) with controllerutil.SetControllerReference for both Role and RoleBinding. The old helper read the GVK from the object's metadata and replaced all owner references unconditionally. The new function reads the GVK from the scheme and appends to existing owner references, refusing to overwrite if another controller already owns the object. Both produce identical results for our use case since the Role is always freshly built. The GVK is now resolved from the scheme configured via CUSTOM_CNPG_GROUP/CUSTOM_CNPG_VERSION, which must match the actual CNPG API group (same requirement as the instance sidecar). Add dynamic CNPG scheme registration (internal/scheme) to the operator, instance, and restore managers, replacing hardcoded cnpgv1.AddToScheme calls. Add RBAC permission for the plugin to list/watch Clusters. Signed-off-by: Armando Ruocco <armando.ruocco@enterprisedb.com>
This commit is contained in:
parent
b1f373d84b
commit
fe445b154f
@ -44,6 +44,7 @@ rules:
|
||||
- postgresql.cnpg.io
|
||||
resources:
|
||||
- backups
|
||||
- clusters
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
|
||||
@ -102,6 +102,8 @@ func NewCmd() *cobra.Command {
|
||||
_ = viper.BindPFlag("server-address", cmd.Flags().Lookup("server-address"))
|
||||
|
||||
_ = viper.BindEnv("sidecar-image", "SIDECAR_IMAGE")
|
||||
_ = viper.BindEnv("custom-cnpg-group", "CUSTOM_CNPG_GROUP")
|
||||
_ = viper.BindEnv("custom-cnpg-version", "CUSTOM_CNPG_VERSION")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
@ -24,7 +24,6 @@ import (
|
||||
"crypto/tls"
|
||||
|
||||
// +kubebuilder:scaffold:imports
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
@ -38,25 +37,33 @@ import (
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/controller"
|
||||
pluginscheme "github.com/cloudnative-pg/plugin-barman-cloud/internal/scheme"
|
||||
|
||||
// Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.)
|
||||
// to ensure that exec-entrypoint and run can make use of them.
|
||||
_ "k8s.io/client-go/plugin/pkg/client/auth"
|
||||
)
|
||||
|
||||
var scheme = runtime.NewScheme()
|
||||
// generateScheme creates a runtime.Scheme with all type definitions
|
||||
// needed by the operator. CNPG types are registered under a
|
||||
// configurable API group to support custom CNPG-based operators.
|
||||
func generateScheme(ctx context.Context) *runtime.Scheme {
|
||||
result := runtime.NewScheme()
|
||||
|
||||
func init() {
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||
utilruntime.Must(barmancloudv1.AddToScheme(scheme))
|
||||
utilruntime.Must(cnpgv1.AddToScheme(scheme))
|
||||
utilruntime.Must(clientgoscheme.AddToScheme(result))
|
||||
utilruntime.Must(barmancloudv1.AddToScheme(result))
|
||||
pluginscheme.AddCNPGToScheme(ctx, result)
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Start starts the manager
|
||||
func Start(ctx context.Context) error {
|
||||
setupLog := log.FromContext(ctx)
|
||||
|
||||
scheme := generateScheme(ctx)
|
||||
|
||||
var tlsOpts []func(*tls.Config)
|
||||
|
||||
// if the enable-http2 flag is false (the default), http/2 should be disabled
|
||||
|
||||
@ -1,58 +0,0 @@
|
||||
/*
|
||||
Copyright © contributors to CloudNativePG, established as
|
||||
CloudNativePG a Series of LF Projects, LLC.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package operator
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/utils/ptr"
|
||||
)
|
||||
|
||||
// setOwnerReference explicitly set the owner reference between an
|
||||
// owner object and a controller one.
|
||||
//
|
||||
// Important: this function won't use any registered scheme and will
|
||||
// fail unless the metadata has been correctly set into the owner
|
||||
// object.
|
||||
func setOwnerReference(owner, controlled metav1.Object) error {
|
||||
ro, ok := owner.(runtime.Object)
|
||||
if !ok {
|
||||
return fmt.Errorf("%T is not a runtime.Object, cannot call setOwnerReference", owner)
|
||||
}
|
||||
|
||||
if len(ro.DeepCopyObject().GetObjectKind().GroupVersionKind().Group) == 0 {
|
||||
return fmt.Errorf("%T metadata have not been set, cannot call setOwnerReference", owner)
|
||||
}
|
||||
|
||||
controlled.SetOwnerReferences([]metav1.OwnerReference{
|
||||
{
|
||||
APIVersion: ro.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
||||
Kind: ro.GetObjectKind().GroupVersionKind().Kind,
|
||||
Name: owner.GetName(),
|
||||
UID: owner.GetUID(),
|
||||
BlockOwnerDeletion: ptr.To(true),
|
||||
Controller: ptr.To(true),
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
158
internal/cnpgi/operator/rbac/ensure.go
Normal file
158
internal/cnpgi/operator/rbac/ensure.go
Normal file
@ -0,0 +1,158 @@
|
||||
/*
|
||||
Copyright © contributors to CloudNativePG, established as
|
||||
CloudNativePG a Series of LF Projects, LLC.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Package rbac contains utilities to reconcile RBAC resources
|
||||
// for the barman-cloud plugin.
|
||||
package rbac
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||
)
|
||||
|
||||
// EnsureRole ensures the RBAC Role for the given Cluster matches
|
||||
// the desired state derived from the given ObjectStores. On creation,
|
||||
// the Cluster is set as the owner of the Role for garbage collection.
|
||||
//
|
||||
// This function is called from both the Pre hook (gRPC) and the
|
||||
// ObjectStore controller. To handle concurrent modifications
|
||||
// gracefully, AlreadyExists on Create and Conflict on Patch are
|
||||
// retried once rather than returned as errors.
|
||||
func EnsureRole(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
cluster *cnpgv1.Cluster,
|
||||
barmanObjects []barmancloudv1.ObjectStore,
|
||||
) error {
|
||||
newRole := specs.BuildRole(cluster, barmanObjects)
|
||||
|
||||
roleKey := client.ObjectKey{
|
||||
Namespace: newRole.Namespace,
|
||||
Name: newRole.Name,
|
||||
}
|
||||
|
||||
var role rbacv1.Role
|
||||
err := c.Get(ctx, roleKey, &role)
|
||||
|
||||
switch {
|
||||
case apierrs.IsNotFound(err):
|
||||
role, err := createRole(ctx, c, cluster, newRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if role == nil {
|
||||
// Created successfully, nothing else to do.
|
||||
return nil
|
||||
}
|
||||
// AlreadyExists: fall through to patch with the re-read role.
|
||||
return patchRoleRules(ctx, c, newRole.Rules, role)
|
||||
|
||||
case err != nil:
|
||||
return err
|
||||
|
||||
default:
|
||||
return patchRoleRules(ctx, c, newRole.Rules, &role)
|
||||
}
|
||||
}
|
||||
|
||||
// createRole attempts to create the Role. If another writer created
|
||||
// it concurrently (AlreadyExists), it re-reads and returns the
|
||||
// existing Role for the caller to patch. On success it returns nil.
|
||||
func createRole(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
cluster *cnpgv1.Cluster,
|
||||
newRole *rbacv1.Role,
|
||||
) (*rbacv1.Role, error) {
|
||||
contextLogger := log.FromContext(ctx)
|
||||
|
||||
if err := controllerutil.SetControllerReference(cluster, newRole, c.Scheme()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
contextLogger.Info("Creating role",
|
||||
"name", newRole.Name, "namespace", newRole.Namespace)
|
||||
|
||||
createErr := c.Create(ctx, newRole)
|
||||
if createErr == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if !apierrs.IsAlreadyExists(createErr) {
|
||||
return nil, createErr
|
||||
}
|
||||
|
||||
contextLogger.Info("Role was created concurrently, checking rules")
|
||||
|
||||
var role rbacv1.Role
|
||||
if err := c.Get(ctx, client.ObjectKeyFromObject(newRole), &role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &role, nil
|
||||
}
|
||||
|
||||
// patchRoleRules patches the Role's rules if they differ from the
|
||||
// desired state. On Conflict (concurrent modification), it retries
|
||||
// once with a fresh read.
|
||||
func patchRoleRules(
|
||||
ctx context.Context,
|
||||
c client.Client,
|
||||
desiredRules []rbacv1.PolicyRule,
|
||||
role *rbacv1.Role,
|
||||
) error {
|
||||
if equality.Semantic.DeepEqual(desiredRules, role.Rules) {
|
||||
return nil
|
||||
}
|
||||
|
||||
contextLogger := log.FromContext(ctx)
|
||||
contextLogger.Info("Patching role",
|
||||
"name", role.Name, "namespace", role.Namespace, "rules", desiredRules)
|
||||
|
||||
oldRole := role.DeepCopy()
|
||||
role.Rules = desiredRules
|
||||
|
||||
patchErr := c.Patch(ctx, role, client.MergeFrom(oldRole))
|
||||
if patchErr == nil || !apierrs.IsConflict(patchErr) {
|
||||
return patchErr
|
||||
}
|
||||
|
||||
// Conflict: re-read and retry once.
|
||||
contextLogger.Info("Role was modified concurrently, retrying patch")
|
||||
if err := c.Get(ctx, client.ObjectKeyFromObject(role), role); err != nil {
|
||||
return err
|
||||
}
|
||||
if equality.Semantic.DeepEqual(desiredRules, role.Rules) {
|
||||
return nil
|
||||
}
|
||||
|
||||
oldRole = role.DeepCopy()
|
||||
role.Rules = desiredRules
|
||||
|
||||
return c.Patch(ctx, role, client.MergeFrom(oldRole))
|
||||
}
|
||||
175
internal/cnpgi/operator/rbac/ensure_test.go
Normal file
175
internal/cnpgi/operator/rbac/ensure_test.go
Normal file
@ -0,0 +1,175 @@
|
||||
/*
|
||||
Copyright © contributors to CloudNativePG, established as
|
||||
CloudNativePG a Series of LF Projects, LLC.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package rbac_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||
)
|
||||
|
||||
func newScheme() *runtime.Scheme {
|
||||
s := runtime.NewScheme()
|
||||
_ = rbacv1.AddToScheme(s)
|
||||
_ = cnpgv1.AddToScheme(s)
|
||||
_ = barmancloudv1.AddToScheme(s)
|
||||
return s
|
||||
}
|
||||
|
||||
func newCluster(name, namespace string) *cnpgv1.Cluster {
|
||||
return &cnpgv1.Cluster{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newObjectStore(name, namespace, secretName string) barmancloudv1.ObjectStore {
|
||||
return barmancloudv1.ObjectStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: barmancloudv1.ObjectStoreSpec{
|
||||
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||
DestinationPath: "s3://bucket/path",
|
||||
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||
AWS: &barmanapi.S3Credentials{
|
||||
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||
LocalObjectReference: machineryapi.LocalObjectReference{
|
||||
Name: secretName,
|
||||
},
|
||||
Key: "ACCESS_KEY_ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = Describe("EnsureRole", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
cluster *cnpgv1.Cluster
|
||||
objects []barmancloudv1.ObjectStore
|
||||
fakeClient client.Client
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
cluster = newCluster("test-cluster", "default")
|
||||
objects = []barmancloudv1.ObjectStore{
|
||||
newObjectStore("my-store", "default", "aws-creds"),
|
||||
}
|
||||
})
|
||||
|
||||
Context("when the Role does not exist", func() {
|
||||
BeforeEach(func() {
|
||||
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||
})
|
||||
|
||||
It("should create the Role with owner reference", func() {
|
||||
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var role rbacv1.Role
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "test-cluster-barman-cloud",
|
||||
}, &role)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(role.Rules).To(HaveLen(3))
|
||||
|
||||
// Verify owner reference is set to the Cluster
|
||||
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||
Expect(role.OwnerReferences[0].Name).To(Equal("test-cluster"))
|
||||
Expect(role.OwnerReferences[0].Kind).To(Equal("Cluster"))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the Role exists with matching rules", func() {
|
||||
BeforeEach(func() {
|
||||
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, objects)).To(Succeed())
|
||||
})
|
||||
|
||||
It("should not patch the Role", func() {
|
||||
var before rbacv1.Role
|
||||
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "test-cluster-barman-cloud",
|
||||
}, &before)).To(Succeed())
|
||||
|
||||
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var after rbacv1.Role
|
||||
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "test-cluster-barman-cloud",
|
||||
}, &after)).To(Succeed())
|
||||
|
||||
Expect(after.ResourceVersion).To(Equal(before.ResourceVersion))
|
||||
})
|
||||
})
|
||||
|
||||
Context("when the Role exists with different rules", func() {
|
||||
BeforeEach(func() {
|
||||
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||
oldObjects := []barmancloudv1.ObjectStore{
|
||||
newObjectStore("my-store", "default", "old-secret"),
|
||||
}
|
||||
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, oldObjects)).To(Succeed())
|
||||
})
|
||||
|
||||
It("should patch the Role with new rules and preserve owner reference", func() {
|
||||
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var role rbacv1.Role
|
||||
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "test-cluster-barman-cloud",
|
||||
}, &role)).To(Succeed())
|
||||
|
||||
secretsRule := role.Rules[2]
|
||||
Expect(secretsRule.ResourceNames).To(ContainElement("aws-creds"))
|
||||
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||
|
||||
// Owner reference must survive the patch
|
||||
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||
Expect(role.OwnerReferences[0].Name).To(Equal("test-cluster"))
|
||||
})
|
||||
})
|
||||
})
|
||||
32
internal/cnpgi/operator/rbac/suite_test.go
Normal file
32
internal/cnpgi/operator/rbac/suite_test.go
Normal file
@ -0,0 +1,32 @@
|
||||
/*
|
||||
Copyright © contributors to CloudNativePG, established as
|
||||
CloudNativePG a Series of LF Projects, LLC.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
package rbac_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestRBAC(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "RBAC Suite")
|
||||
}
|
||||
@ -28,12 +28,13 @@ import (
|
||||
"github.com/cloudnative-pg/cnpg-i/pkg/reconciler"
|
||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||
)
|
||||
|
||||
@ -113,7 +114,7 @@ func (r ReconcilerImplementation) Pre(
|
||||
barmanObjects = append(barmanObjects, barmanObject)
|
||||
}
|
||||
|
||||
if err := r.ensureRole(ctx, &cluster, barmanObjects); err != nil {
|
||||
if err := rbac.EnsureRole(ctx, r.Client, &cluster, barmanObjects); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -137,57 +138,6 @@ func (r ReconcilerImplementation) Post(
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r ReconcilerImplementation) ensureRole(
|
||||
ctx context.Context,
|
||||
cluster *cnpgv1.Cluster,
|
||||
barmanObjects []barmancloudv1.ObjectStore,
|
||||
) error {
|
||||
contextLogger := log.FromContext(ctx)
|
||||
newRole := specs.BuildRole(cluster, barmanObjects)
|
||||
|
||||
var role rbacv1.Role
|
||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
||||
Namespace: newRole.Namespace,
|
||||
Name: newRole.Name,
|
||||
}, &role); err != nil {
|
||||
if !apierrs.IsNotFound(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
contextLogger.Info(
|
||||
"Creating role",
|
||||
"name", newRole.Name,
|
||||
"namespace", newRole.Namespace,
|
||||
)
|
||||
|
||||
if err := setOwnerReference(cluster, newRole); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return r.Client.Create(ctx, newRole)
|
||||
}
|
||||
|
||||
if equality.Semantic.DeepEqual(newRole.Rules, role.Rules) {
|
||||
// There's no need to hit the API server again
|
||||
return nil
|
||||
}
|
||||
|
||||
contextLogger.Info(
|
||||
"Patching role",
|
||||
"name", newRole.Name,
|
||||
"namespace", newRole.Namespace,
|
||||
"rules", newRole.Rules,
|
||||
)
|
||||
|
||||
oldRole := role.DeepCopy()
|
||||
|
||||
// Apply to the role the new rules
|
||||
role.Rules = newRole.Rules
|
||||
|
||||
// Push it back to the API server
|
||||
return r.Client.Patch(ctx, &role, client.MergeFrom(oldRole))
|
||||
}
|
||||
|
||||
func (r ReconcilerImplementation) ensureRoleBinding(
|
||||
ctx context.Context,
|
||||
cluster *cnpgv1.Cluster,
|
||||
@ -213,7 +163,7 @@ func (r ReconcilerImplementation) createRoleBinding(
|
||||
cluster *cnpgv1.Cluster,
|
||||
) error {
|
||||
roleBinding := specs.BuildRoleBinding(cluster)
|
||||
if err := setOwnerReference(cluster, roleBinding); err != nil {
|
||||
if err := controllerutil.SetControllerReference(cluster, roleBinding, r.Client.Scheme()); err != nil {
|
||||
return err
|
||||
}
|
||||
return r.Client.Create(ctx, roleBinding)
|
||||
|
||||
@ -32,6 +32,7 @@ import (
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/common"
|
||||
)
|
||||
|
||||
|
||||
// Start starts the sidecar informers and CNPG-i server
|
||||
func Start(ctx context.Context) error {
|
||||
setupLog := log.FromContext(ctx)
|
||||
|
||||
@ -23,12 +23,18 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||
)
|
||||
|
||||
// ObjectStoreReconciler reconciles a ObjectStore object.
|
||||
@ -40,33 +46,96 @@ type ObjectStoreReconciler struct {
|
||||
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;patch;update;get;list;watch
|
||||
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;patch;update;get;list;watch
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=create;list;get;watch;delete
|
||||
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=clusters,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=clusters/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=backups,verbs=get;list;watch
|
||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/finalizers,verbs=update
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the ObjectStore object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile
|
||||
func (r *ObjectStoreReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
// Reconcile ensures that the RBAC Role for each Cluster referencing
|
||||
// this ObjectStore is up to date with the current ObjectStore spec.
|
||||
func (r *ObjectStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
contextLogger := log.FromContext(ctx).WithValues(
|
||||
"objectStoreName", req.Name,
|
||||
"namespace", req.Namespace,
|
||||
)
|
||||
ctx = log.IntoContext(ctx, contextLogger)
|
||||
|
||||
// TODO(user): your logic here
|
||||
contextLogger.Info("ObjectStore reconciliation start")
|
||||
|
||||
// List all Clusters in the same namespace
|
||||
var clusterList cnpgv1.ClusterList
|
||||
if err := r.List(ctx, &clusterList, client.InNamespace(req.Namespace)); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("while listing clusters: %w", err)
|
||||
}
|
||||
|
||||
// For each Cluster that references this ObjectStore, reconcile the Role
|
||||
for i := range clusterList.Items {
|
||||
cluster := &clusterList.Items[i]
|
||||
|
||||
pluginConfiguration := config.NewFromCluster(cluster)
|
||||
referredObjects := pluginConfiguration.GetReferredBarmanObjectsKey()
|
||||
|
||||
if !referencesObjectStore(referredObjects, req.NamespacedName) {
|
||||
continue
|
||||
}
|
||||
|
||||
contextLogger.Info("Reconciling RBAC for cluster",
|
||||
"clusterName", cluster.Name)
|
||||
|
||||
if err := r.reconcileRBACForCluster(ctx, cluster, referredObjects); err != nil {
|
||||
return ctrl.Result{}, fmt.Errorf("while reconciling RBAC for cluster %s: %w", cluster.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
contextLogger.Info("ObjectStore reconciliation completed")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// reconcileRBACForCluster ensures the Role for the given Cluster is
|
||||
// up to date with the current ObjectStore specs.
|
||||
func (r *ObjectStoreReconciler) reconcileRBACForCluster(
|
||||
ctx context.Context,
|
||||
cluster *cnpgv1.Cluster,
|
||||
referredObjectKeys []client.ObjectKey,
|
||||
) error {
|
||||
contextLogger := log.FromContext(ctx)
|
||||
barmanObjects := make([]barmancloudv1.ObjectStore, 0, len(referredObjectKeys))
|
||||
for _, key := range referredObjectKeys {
|
||||
var barmanObject barmancloudv1.ObjectStore
|
||||
if err := r.Get(ctx, key, &barmanObject); err != nil {
|
||||
if apierrs.IsNotFound(err) {
|
||||
contextLogger.Info("ObjectStore not found, skipping",
|
||||
"objectStoreName", key.Name)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("while getting ObjectStore %s: %w", key, err)
|
||||
}
|
||||
barmanObjects = append(barmanObjects, barmanObject)
|
||||
}
|
||||
|
||||
return rbac.EnsureRole(ctx, r.Client, cluster, barmanObjects)
|
||||
}
|
||||
|
||||
// referencesObjectStore checks if the given ObjectStore is in the list
|
||||
// of referred barman objects.
|
||||
func referencesObjectStore(
|
||||
referredObjects []client.ObjectKey,
|
||||
objectStore client.ObjectKey,
|
||||
) bool {
|
||||
for _, ref := range referredObjects {
|
||||
if ref.Name == objectStore.Name && ref.Namespace == objectStore.Namespace {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *ObjectStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
err := ctrl.NewControllerManagedBy(mgr).
|
||||
For(&barmancloudv1.ObjectStore{}).
|
||||
For(&barmancloudv1.ObjectStore{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
|
||||
Complete(r)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create controller: %w", err)
|
||||
|
||||
@ -22,70 +22,301 @@ package controller
|
||||
import (
|
||||
"context"
|
||||
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
rbacv1 "k8s.io/api/rbac/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||
)
|
||||
|
||||
var _ = Describe("ObjectStore Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
func newFakeScheme() *runtime.Scheme {
|
||||
s := runtime.NewScheme()
|
||||
_ = rbacv1.AddToScheme(s)
|
||||
_ = cnpgv1.AddToScheme(s)
|
||||
_ = barmancloudv1.AddToScheme(s)
|
||||
return s
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
func newTestCluster(name, namespace, objectStoreName string) *cnpgv1.Cluster {
|
||||
return &cnpgv1.Cluster{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: cnpgv1.ClusterSpec{
|
||||
Plugins: []cnpgv1.PluginConfiguration{
|
||||
{
|
||||
Name: metadata.PluginName,
|
||||
Parameters: map[string]string{
|
||||
"barmanObjectName": objectStoreName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
func newTestObjectStore(name, namespace, secretName string) *barmancloudv1.ObjectStore {
|
||||
return &barmancloudv1.ObjectStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
},
|
||||
Spec: barmancloudv1.ObjectStoreSpec{
|
||||
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||
DestinationPath: "s3://bucket/path",
|
||||
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||
AWS: &barmanapi.S3Credentials{
|
||||
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||
LocalObjectReference: machineryapi.LocalObjectReference{
|
||||
Name: secretName,
|
||||
},
|
||||
Key: "ACCESS_KEY_ID",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var _ = Describe("referencesObjectStore", func() {
|
||||
It("should return true when ObjectStore is in the list", func() {
|
||||
refs := []client.ObjectKey{
|
||||
{Name: "store-a", Namespace: "default"},
|
||||
{Name: "store-b", Namespace: "default"},
|
||||
}
|
||||
objectstore := &barmancloudv1.ObjectStore{}
|
||||
Expect(referencesObjectStore(refs, client.ObjectKey{
|
||||
Name: "store-b", Namespace: "default",
|
||||
})).To(BeTrue())
|
||||
})
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind ObjectStore")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, objectstore)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &barmancloudv1.ObjectStore{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
Spec: barmancloudv1.ObjectStoreSpec{
|
||||
Configuration: barmanapi.BarmanObjectStoreConfiguration{DestinationPath: "/tmp"},
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
It("should return false when ObjectStore is not in the list", func() {
|
||||
refs := []client.ObjectKey{
|
||||
{Name: "store-a", Namespace: "default"},
|
||||
}
|
||||
Expect(referencesObjectStore(refs, client.ObjectKey{
|
||||
Name: "store-b", Namespace: "default",
|
||||
})).To(BeFalse())
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &barmancloudv1.ObjectStore{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
It("should return false when namespace differs", func() {
|
||||
refs := []client.ObjectKey{
|
||||
{Name: "store-a", Namespace: "ns1"},
|
||||
}
|
||||
Expect(referencesObjectStore(refs, client.ObjectKey{
|
||||
Name: "store-a", Namespace: "ns2",
|
||||
})).To(BeFalse())
|
||||
})
|
||||
|
||||
By("Cleanup the specific resource instance ObjectStore")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &ObjectStoreReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
It("should return false for empty list", func() {
|
||||
Expect(referencesObjectStore(nil, client.ObjectKey{
|
||||
Name: "store-a", Namespace: "default",
|
||||
})).To(BeFalse())
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("ObjectStoreReconciler", func() {
|
||||
var (
|
||||
ctx context.Context
|
||||
scheme *runtime.Scheme
|
||||
)
|
||||
|
||||
BeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
scheme = newFakeScheme()
|
||||
})
|
||||
|
||||
Describe("Reconcile", func() {
|
||||
It("should create a Role for a Cluster that references the ObjectStore", func() {
|
||||
objectStore := newTestObjectStore("my-store", "default", "aws-creds")
|
||||
cluster := newTestCluster("my-cluster", "default", "my-store")
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(objectStore, cluster).
|
||||
Build()
|
||||
|
||||
reconciler := &ObjectStoreReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "my-store",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
Expect(result).To(Equal(reconcile.Result{}))
|
||||
|
||||
var role rbacv1.Role
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "my-cluster-barman-cloud",
|
||||
}, &role)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(role.Rules).To(HaveLen(3))
|
||||
|
||||
// Verify the secrets rule contains the expected secret
|
||||
secretsRule := role.Rules[2]
|
||||
Expect(secretsRule.ResourceNames).To(ContainElement("aws-creds"))
|
||||
|
||||
// Verify owner reference is set to the Cluster
|
||||
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||
Expect(role.OwnerReferences[0].Name).To(Equal("my-cluster"))
|
||||
Expect(role.OwnerReferences[0].Kind).To(Equal("Cluster"))
|
||||
})
|
||||
|
||||
It("should skip Clusters that don't reference the ObjectStore", func() {
|
||||
objectStore := newTestObjectStore("my-store", "default", "aws-creds")
|
||||
cluster := newTestCluster("my-cluster", "default", "other-store")
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(objectStore, cluster).
|
||||
Build()
|
||||
|
||||
reconciler := &ObjectStoreReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "my-store",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(reconcile.Result{}))
|
||||
|
||||
// No Role should have been created
|
||||
var role rbacv1.Role
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "my-cluster-barman-cloud",
|
||||
}, &role)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
|
||||
It("should succeed with no Clusters in the namespace", func() {
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
Build()
|
||||
|
||||
reconciler := &ObjectStoreReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: "my-store",
|
||||
Namespace: "default",
|
||||
},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(result).To(Equal(reconcile.Result{}))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("reconcileRBACForCluster", func() {
|
||||
It("should skip deleted ObjectStores and still reconcile the Role", func() {
|
||||
// Cluster references two ObjectStores, but one is deleted
|
||||
cluster := newTestCluster("my-cluster", "default", "store-a")
|
||||
existingStore := newTestObjectStore("store-a", "default", "aws-creds")
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(existingStore).
|
||||
Build()
|
||||
|
||||
reconciler := &ObjectStoreReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
// Pass two keys, but "store-b" doesn't exist
|
||||
err := reconciler.reconcileRBACForCluster(ctx, cluster, []client.ObjectKey{
|
||||
{Name: "store-a", Namespace: "default"},
|
||||
{Name: "store-b", Namespace: "default"},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Role should be created with only store-a's secrets
|
||||
var role rbacv1.Role
|
||||
err = fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "my-cluster-barman-cloud",
|
||||
}, &role)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(role.Rules).To(HaveLen(3))
|
||||
|
||||
// ObjectStore rule should only reference store-a
|
||||
objectStoreRule := role.Rules[0]
|
||||
Expect(objectStoreRule.ResourceNames).To(ContainElement("store-a"))
|
||||
Expect(objectStoreRule.ResourceNames).NotTo(ContainElement("store-b"))
|
||||
|
||||
// Verify owner reference is set
|
||||
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||
Expect(role.OwnerReferences[0].Name).To(Equal("my-cluster"))
|
||||
})
|
||||
|
||||
It("should update Role when ObjectStore credentials change", func() {
|
||||
cluster := newTestCluster("my-cluster", "default", "my-store")
|
||||
oldStore := newTestObjectStore("my-store", "default", "old-secret")
|
||||
|
||||
fakeClient := fake.NewClientBuilder().
|
||||
WithScheme(scheme).
|
||||
WithObjects(oldStore).
|
||||
Build()
|
||||
|
||||
reconciler := &ObjectStoreReconciler{
|
||||
Client: fakeClient,
|
||||
Scheme: scheme,
|
||||
}
|
||||
|
||||
// First reconcile - creates Role with old-secret
|
||||
err := reconciler.reconcileRBACForCluster(ctx, cluster, []client.ObjectKey{
|
||||
{Name: "my-store", Namespace: "default"},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// Update the ObjectStore with new credentials
|
||||
var currentStore barmancloudv1.ObjectStore
|
||||
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||
Name: "my-store", Namespace: "default",
|
||||
}, ¤tStore)).To(Succeed())
|
||||
currentStore.Spec.Configuration.BarmanCredentials.AWS.AccessKeyIDReference.LocalObjectReference.Name = "new-secret"
|
||||
Expect(fakeClient.Update(ctx, ¤tStore)).To(Succeed())
|
||||
|
||||
// Second reconcile - should patch Role with new-secret
|
||||
err = reconciler.reconcileRBACForCluster(ctx, cluster, []client.ObjectKey{
|
||||
{Name: "my-store", Namespace: "default"},
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
var role rbacv1.Role
|
||||
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||
Namespace: "default",
|
||||
Name: "my-cluster-barman-cloud",
|
||||
}, &role)).To(Succeed())
|
||||
|
||||
secretsRule := role.Rules[2]
|
||||
Expect(secretsRule.ResourceNames).To(ContainElement("new-secret"))
|
||||
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -20,81 +20,14 @@ SPDX-License-Identifier: Apache-2.0
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
// +kubebuilder:scaffold:imports
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var (
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
testEnv *envtest.Environment
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
)
|
||||
|
||||
func TestControllers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
ctx, cancel = context.WithCancel(context.TODO())
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
|
||||
// The BinaryAssetsDirectory is only required if you want to run the tests directly
|
||||
// without call the makefile target test. If not informed it will look for the
|
||||
// default path defined in controller-runtime which is /usr/local/kubebuilder/.
|
||||
// Note that you must have the required binaries setup under the bin directory to perform
|
||||
// the tests directly. When we run make test it will be setup and used automatically.
|
||||
BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s",
|
||||
fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = barmancloudv1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
cancel()
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
58
internal/scheme/cnpg.go
Normal file
58
internal/scheme/cnpg.go
Normal file
@ -0,0 +1,58 @@
|
||||
/*
|
||||
Copyright © contributors to CloudNativePG, established as
|
||||
CloudNativePG a Series of LF Projects, LLC.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
// Package scheme provides utilities for building runtime schemes
|
||||
// with support for custom CNPG API groups.
|
||||
package scheme
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
crscheme "sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||
)
|
||||
|
||||
// AddCNPGToScheme registers CNPG types into the given scheme using
|
||||
// the API group configured via CUSTOM_CNPG_GROUP/CUSTOM_CNPG_VERSION
|
||||
// environment variables, defaulting to postgresql.cnpg.io/v1.
|
||||
// This allows the plugin to work with any CNPG-based operator.
|
||||
func AddCNPGToScheme(ctx context.Context, s *runtime.Scheme) {
|
||||
cnpgGroup := viper.GetString("custom-cnpg-group")
|
||||
cnpgVersion := viper.GetString("custom-cnpg-version")
|
||||
if len(cnpgGroup) == 0 {
|
||||
cnpgGroup = cnpgv1.SchemeGroupVersion.Group
|
||||
}
|
||||
if len(cnpgVersion) == 0 {
|
||||
cnpgVersion = cnpgv1.SchemeGroupVersion.Version
|
||||
}
|
||||
|
||||
schemeGroupVersion := schema.GroupVersion{Group: cnpgGroup, Version: cnpgVersion}
|
||||
schemeBuilder := &crscheme.Builder{GroupVersion: schemeGroupVersion}
|
||||
schemeBuilder.Register(&cnpgv1.Cluster{}, &cnpgv1.ClusterList{})
|
||||
schemeBuilder.Register(&cnpgv1.Backup{}, &cnpgv1.BackupList{})
|
||||
schemeBuilder.Register(&cnpgv1.ScheduledBackup{}, &cnpgv1.ScheduledBackupList{})
|
||||
utilruntime.Must(schemeBuilder.AddToScheme(s))
|
||||
|
||||
log.FromContext(ctx).Info("CNPG types registration", "schemeGroupVersion", schemeGroupVersion)
|
||||
}
|
||||
@ -870,6 +870,7 @@ rules:
|
||||
- postgresql.cnpg.io
|
||||
resources:
|
||||
- backups
|
||||
- clusters
|
||||
verbs:
|
||||
- get
|
||||
- list
|
||||
|
||||
Loading…
Reference in New Issue
Block a user