Skip to content

Commit

Permalink
Add role component
Browse files Browse the repository at this point in the history
- Remove Role from template yaml from chart
- Add utils function `GetRoleName`
- Removed old `reconcileRole`
- Add unit tests for Role component
  • Loading branch information
seshachalam-yv committed Apr 5, 2023
1 parent 5a52a5c commit 823bf18
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 82 deletions.
5 changes: 5 additions & 0 deletions api/v1alpha1/types_etcd.go
Original file line number Diff line number Diff line change
Expand Up @@ -470,3 +470,8 @@ func (e *Etcd) GetAsOwnerReference() metav1.OwnerReference {
BlockOwnerDeletion: pointer.Bool(true),
}
}

// GetRoleName returns the role name for the Etcd
func (e *Etcd) GetRoleName() string {
return fmt.Sprintf("druid.gardener.cloud:etcd:%s", e.Name)
}
47 changes: 0 additions & 47 deletions charts/etcd/templates/etcd-role.yaml

This file was deleted.

45 changes: 11 additions & 34 deletions controllers/etcd/reconciler.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
componentconfigmap "github.com/gardener/etcd-druid/pkg/component/etcd/configmap"
componentlease "github.com/gardener/etcd-druid/pkg/component/etcd/lease"
componentpdb "github.com/gardener/etcd-druid/pkg/component/etcd/poddisruptionbudget"
componentrole "github.com/gardener/etcd-druid/pkg/component/etcd/role"
componentservice "github.com/gardener/etcd-druid/pkg/component/etcd/service"
componentserviceaccount "github.com/gardener/etcd-druid/pkg/component/etcd/serviceaccount"
componentsts "github.com/gardener/etcd-druid/pkg/component/etcd/statefulset"
Expand Down Expand Up @@ -271,6 +272,14 @@ func (r *Reconciler) delete(ctx context.Context, etcd *druidv1alpha1.Etcd) (ctrl
}, err
}

roleValues := componentrole.GenerateValues(etcd)
roleDeployer := componentrole.New(r.Client, roleValues)
if err := roleDeployer.Destroy(ctx); err != nil {
return ctrl.Result{
Requeue: true,
}, err
}

if sets.NewString(etcd.Finalizers...).Has(common.FinalizerName) {
logger.Info("Removing finalizer")
if err := controllerutils.RemoveFinalizers(ctx, r.Client, etcd, common.FinalizerName); client.IgnoreNotFound(err) != nil {
Expand All @@ -283,39 +292,6 @@ func (r *Reconciler) delete(ctx context.Context, etcd *druidv1alpha1.Etcd) (ctrl
return ctrl.Result{}, nil
}

func (r *Reconciler) reconcileRole(ctx context.Context, logger logr.Logger, etcd *druidv1alpha1.Etcd, values map[string]interface{}) error {
logger.Info("Reconciling role")
var err error

decoded, err := r.chart.decodeRole(etcd.Name, etcd.Namespace, values)
if err != nil {
return err
}

roleObj := &rbac.Role{}
key := client.ObjectKeyFromObject(decoded)
if err := r.Get(ctx, key, roleObj); err != nil {
if !apierrors.IsNotFound(err) {
return err
}
if err := r.Create(ctx, decoded); err != nil {
return err
}
logger.Info("Creating role", "role", kutil.Key(decoded.Namespace, decoded.Name).String())
return nil
}

if !reflect.DeepEqual(decoded.Rules, roleObj.Rules) {
roleObjCopy := roleObj.DeepCopy()
roleObjCopy.Rules = decoded.Rules
if err := r.Patch(ctx, roleObjCopy, client.MergeFrom(roleObj)); err != nil {
return err
}
}

return nil
}

func (r *Reconciler) reconcileRoleBinding(ctx context.Context, logger logr.Logger, etcd *druidv1alpha1.Etcd, values map[string]interface{}) error {
logger.Info("Reconciling rolebinding")
var err error
Expand Down Expand Up @@ -403,7 +379,8 @@ func (r *Reconciler) reconcileEtcd(ctx context.Context, logger logr.Logger, etcd
return reconcileResult{err: err}
}

err = r.reconcileRole(ctx, logger, etcd, roleValues)
roleDeployer := componentrole.New(r.Client, componentrole.GenerateValues(etcd))
err = roleDeployer.Deploy(ctx)
if err != nil {
return reconcileResult{err: err}
}
Expand Down
85 changes: 85 additions & 0 deletions pkg/component/etcd/role/role.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// 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.

package role

import (
"context"

"github.com/gardener/gardener/pkg/controllerutils"

gardenercomponent "github.com/gardener/gardener/pkg/operation/botanist/component"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

type component struct {
client client.Client
values *Values
}

func (c component) Deploy(ctx context.Context) error {
role := c.emptyRole()
_, err := controllerutils.GetAndCreateOrStrategicMergePatch(ctx, c.client, role, func() error {
role.Name = c.values.Name
role.Namespace = c.values.Namespace
role.Labels = c.values.Labels
role.OwnerReferences = c.values.OwnerReferences
role.Rules = getRules()
return nil
})
return err
}

func (c component) Destroy(ctx context.Context) error {
return client.IgnoreNotFound(c.client.Delete(ctx, c.emptyRole()))
}

func (c component) emptyRole() *rbacv1.Role {
return &rbacv1.Role{
ObjectMeta: metav1.ObjectMeta{
Name: c.values.Name,
Namespace: c.values.Namespace,
},
}
}

func getRules() []rbacv1.PolicyRule {
return []rbacv1.PolicyRule{
{
APIGroups: []string{"coordination.k8s.io"},
Resources: []string{"leases"},
Verbs: []string{"list", "get", "update", "patch", "watch"},
},
{
APIGroups: []string{"apps"},
Resources: []string{"statefulsets"},
Verbs: []string{"get", "list", "patch", "update", "watch"},
},
{
APIGroups: []string{""},
Resources: []string{"pods"},
Verbs: []string{"get", "list", "watch"},
},
}
}

// New creates a new role deployer instance.
func New(c client.Client, values *Values) gardenercomponent.Deployer {
return &component{
client: c,
values: values,
}
}
27 changes: 27 additions & 0 deletions pkg/component/etcd/role/role_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// 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.

package role

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestService(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Role Component Suite")
}
146 changes: 146 additions & 0 deletions pkg/component/etcd/role/role_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright (c) 2023 SAP SE or an SAP affiliate company. All rights reserved. This file is licensed under the Apache Software License, v. 2 except as noted otherwise in the LICENSE file
//
// 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.

package role_test

import (
"context"

"github.com/gardener/etcd-druid/api/v1alpha1"
"github.com/gardener/etcd-druid/pkg/client/kubernetes"
"github.com/gardener/etcd-druid/pkg/component/etcd/role"

. "github.com/gardener/gardener/pkg/utils/test/matchers"
. "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/types"
"k8s.io/utils/pointer"

"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

var _ = Describe("Role Component", Ordered, func() {
var (
ctx = context.TODO()
c = fake.NewClientBuilder().WithScheme(kubernetes.Scheme).Build()
values = getTestRoleValues()
roleComponent = role.New(c, values)
)

Describe("#Deploy", func() {
It("should create the Role with the expected values", func() {
By("creating a Role")
err := roleComponent.Deploy(ctx)
Expect(err).NotTo(HaveOccurred())

By("verifying that the Role is created on the K8s cluster as expected")
created := &rbacv1.Role{}
err = c.Get(ctx, getRoleKeyFromValue(values), created)
Expect(err).NotTo(HaveOccurred())
verifyRoleValues(created, values)
})
It("should update the Role with the expected values", func() {
By("updating the Role")
values.Labels["new"] = "label"
err := roleComponent.Deploy(ctx)
Expect(err).NotTo(HaveOccurred())

By("verifying that the Role is updated on the K8s cluster as expected")
updated := &rbacv1.Role{}
err = c.Get(ctx, getRoleKeyFromValue(values), updated)
Expect(err).NotTo(HaveOccurred())
verifyRoleValues(updated, values)
})
It("should not return an error when there is nothing to update the Role", func() {
err := roleComponent.Deploy(ctx)
Expect(err).NotTo(HaveOccurred())
updated := &rbacv1.Role{}
err = c.Get(ctx, getRoleKeyFromValue(values), updated)
Expect(err).NotTo(HaveOccurred())
verifyRoleValues(updated, values)
})
It("should return an error when the update fails", func() {
values.Name = ""
err := roleComponent.Deploy(ctx)
Expect(err).To(HaveOccurred())
Expect(err.Error()).To(ContainSubstring("Required value: name is required"))
})
})

Describe("#Destroy", func() {
It("should delete the Role", func() {
By("deleting the Role")
err := roleComponent.Destroy(ctx)
Expect(err).NotTo(HaveOccurred())

By("verifying that the Role is deleted from the K8s cluster as expected")
role := &rbacv1.Role{}
Expect(c.Get(ctx, getRoleKeyFromValue(values), role)).To(BeNotFoundError())
})
It("should not return an error when there is nothing to delete", func() {
err := roleComponent.Destroy(ctx)
Expect(err).NotTo(HaveOccurred())
})
})
})

func verifyRoleValues(expected *rbacv1.Role, values *role.Values) {
Expect(expected.Name).To(Equal(values.Name))
Expect(expected.Labels).To(Equal(values.Labels))
Expect(expected.Namespace).To(Equal(values.Namespace))
Expect(expected.OwnerReferences).To(Equal(values.OwnerReferences))
Expect(expected.Rules).To(Equal([]rbacv1.PolicyRule{
{
APIGroups: []string{"coordination.k8s.io"},
Resources: []string{"leases"},
Verbs: []string{"list", "get", "update", "patch", "watch"},
},
{
APIGroups: []string{"apps"},
Resources: []string{"statefulsets"},
Verbs: []string{"get", "list", "patch", "update", "watch"},
},
{
APIGroups: []string{""},
Resources: []string{"pods"},
Verbs: []string{"get", "list", "watch"},
},
}))
}
func getRoleKeyFromValue(values *role.Values) types.NamespacedName {
return client.ObjectKey{Name: values.Name, Namespace: values.Namespace}
}

func getTestRoleValues() *role.Values {
return &role.Values{
Name: "test-role",
Namespace: "test-namespace",
Labels: map[string]string{
"foo": "bar",
},
OwnerReferences: []metav1.OwnerReference{
{
APIVersion: v1alpha1.GroupVersion.String(),
Kind: "etcd",
Name: "test-etcd",
UID: "123-456-789",
Controller: pointer.Bool(true),
BlockOwnerDeletion: pointer.Bool(true),
},
},
}
}
Loading

0 comments on commit 823bf18

Please sign in to comment.