Skip to content

Commit

Permalink
Add tests for rbac
Browse files Browse the repository at this point in the history
  • Loading branch information
agouin committed Oct 22, 2023
1 parent 68f982e commit cabd028
Show file tree
Hide file tree
Showing 9 changed files with 341 additions and 20 deletions.
7 changes: 7 additions & 0 deletions internal/fullnode/mock_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

cosmosv1 "github.com/strangelove-ventures/cosmos-operator/api/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
Expand Down Expand Up @@ -87,6 +88,12 @@ func (m *mockClient[T]) List(ctx context.Context, list client.ObjectList, opts .
*ref = m.ObjectList.(corev1.ConfigMapList)
case *corev1.SecretList:
*ref = m.ObjectList.(corev1.SecretList)
case *corev1.ServiceAccountList:
*ref = m.ObjectList.(corev1.ServiceAccountList)
case *rbacv1.RoleList:
*ref = m.ObjectList.(rbacv1.RoleList)
case *rbacv1.RoleBindingList:
*ref = m.ObjectList.(rbacv1.RoleBindingList)
default:
panic(fmt.Errorf("unknown ObjectList type: %T", m.ObjectList))
}
Expand Down
6 changes: 3 additions & 3 deletions internal/fullnode/rbac_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import (
)

func serviceAccountName(crd *cosmosv1.CosmosFullNode) string {
return crd.Name + "-vc"
return crd.Name + "-vc-sa"
}

func roleName(crd *cosmosv1.CosmosFullNode) string {
return crd.Namespace + "-" + crd.Name + "-cr"
return crd.Name + "-vc-r"
}

func roleBindingName(crd *cosmosv1.CosmosFullNode) string {
return crd.Namespace + "-" + crd.Name + "-crb"
return crd.Name + "-vc-rb"
}

// BuildServiceAccounts returns a list of service accounts given the crd.
Expand Down
102 changes: 102 additions & 0 deletions internal/fullnode/rbac_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package fullnode

import (
"testing"

"github.com/stretchr/testify/require"

rbacv1 "k8s.io/api/rbac/v1"
)

func TestBuildRBAC(t *testing.T) {
t.Parallel()

t.Run("build rbac", func(t *testing.T) {
crd := defaultCRD()
crd.Spec.Replicas = 3
crd.Name = "hub"
crd.Namespace = "test"
crd.Spec.ChainSpec.Network = "testnet"
crd.Spec.PodTemplate.Image = "gaia:v6.0.0"

sas := BuildServiceAccounts(&crd)

require.Len(t, sas, 1) // 1 svc account in the namespace

sa := sas[0].Object()

require.Equal(t, "hub-vc-sa", sa.Name)
require.Equal(t, "test", sa.Namespace)

wantLabels := map[string]string{
"app.kubernetes.io/created-by": "cosmos-operator",
"app.kubernetes.io/name": "hub",
"app.kubernetes.io/component": "vc",
"app.kubernetes.io/version": "v6.0.0",
"cosmos.strange.love/network": "testnet",
"cosmos.strange.love/type": "FullNode",
}
require.Equal(t, wantLabels, sa.Labels)

roles := BuildRoles(&crd)

require.Len(t, roles, 1) // 1 role in the namespace

role := roles[0].Object()

require.Equal(t, "hub-vc-r", role.Name)
require.Equal(t, "test", role.Namespace)

wantLabels = map[string]string{
"app.kubernetes.io/created-by": "cosmos-operator",
"app.kubernetes.io/name": "hub",
"app.kubernetes.io/component": "vc",
"app.kubernetes.io/version": "v6.0.0",
"cosmos.strange.love/network": "testnet",
"cosmos.strange.love/type": "FullNode",
}
require.Equal(t, wantLabels, role.Labels)

require.Equal(t, []rbacv1.PolicyRule{
{
APIGroups: []string{""}, // core API group
Resources: []string{"namespaces", "pods"},
Verbs: []string{"get", "list"},
},
{
APIGroups: []string{"cosmos.strange.love"},
Resources: []string{"cosmosfullnodes"},
Verbs: []string{"get"},
},
{
APIGroups: []string{"cosmos.strange.love"},
Resources: []string{"cosmosfullnodes/status"},
Verbs: []string{"update"},
},
}, role.Rules)

rbs := BuildRoleBindings(&crd)

require.Len(t, rbs, 1) // 1 role in the namespace

rb := rbs[0].Object()

require.Equal(t, "hub-vc-rb", rb.Name)
require.Equal(t, "test", rb.Namespace)

wantLabels = map[string]string{
"app.kubernetes.io/created-by": "cosmos-operator",
"app.kubernetes.io/name": "hub",
"app.kubernetes.io/component": "vc",
"app.kubernetes.io/version": "v6.0.0",
"cosmos.strange.love/network": "testnet",
"cosmos.strange.love/type": "FullNode",
}
require.Equal(t, wantLabels, rb.Labels)

require.Len(t, rb.Subjects, 1)
require.Equal(t, rb.Subjects[0].Name, "hub-vc-sa")

require.Equal(t, rb.RoleRef.Name, "hub-vc-r")
})
}
5 changes: 3 additions & 2 deletions internal/fullnode/role_binding_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func NewRoleBindingControl(client Client) RoleBindingControl {
func (sc RoleBindingControl) Reconcile(ctx context.Context, log kube.Logger, crd *cosmosv1.CosmosFullNode) kube.ReconcileError {
var crs rbacv1.RoleBindingList
if err := sc.client.List(ctx, &crs,
client.InNamespace(crd.Namespace),
client.MatchingLabels{
kube.ControllerLabel: "cosmos-operator",
kube.ComponentLabel: "vc",
Expand All @@ -41,7 +42,7 @@ func (sc RoleBindingControl) Reconcile(ctx context.Context, log kube.Logger, crd
diffed := diff.New(current, want)

for _, cr := range diffed.Creates() {
log.Info("Creating role binding", "roleBindingName", cr.Name)
log.Info("Creating role binding", "name", cr.Name)
if err := ctrl.SetControllerReference(crd, cr, sc.client.Scheme()); err != nil {
return kube.TransientError(fmt.Errorf("set controller reference on role binding %q: %w", cr.Name, err))
}
Expand All @@ -53,7 +54,7 @@ func (sc RoleBindingControl) Reconcile(ctx context.Context, log kube.Logger, crd
}

for _, cr := range diffed.Updates() {
log.Info("Updating role binding", "roleBindingName", cr.Name)
log.Info("Updating role binding", "name", cr.Name)
if err := sc.client.Update(ctx, cr); err != nil {
return kube.TransientError(fmt.Errorf("update role binding %q: %w", cr.Name, err))
}
Expand Down
70 changes: 70 additions & 0 deletions internal/fullnode/role_binding_control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package fullnode

import (
"context"
"testing"

"github.com/stretchr/testify/require"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestRoleBindingControl_Reconcile(t *testing.T) {
t.Parallel()

type mockRbClient = mockClient[*rbacv1.RoleBinding]

ctx := context.Background()

t.Run("happy path", func(t *testing.T) {
crd := defaultCRD()
crd.Namespace = "test"
crd.Spec.Replicas = 3

var mClient mockRbClient

control := NewRoleBindingControl(&mClient)
err := control.Reconcile(ctx, nopReporter, &crd)
require.NoError(t, err)

require.Len(t, mClient.GotListOpts, 2)
var listOpt client.ListOptions
for _, opt := range mClient.GotListOpts {
opt.ApplyToList(&listOpt)
}
require.Equal(t, "test", listOpt.Namespace)
require.Zero(t, listOpt.Limit)

require.Equal(t, 1, mClient.CreateCount) // Created 1 role binding.
require.Equal(t, "osmosis-vc-rb", mClient.LastCreateObject.Name)
require.NotEmpty(t, mClient.LastCreateObject.OwnerReferences)
require.Equal(t, crd.Name, mClient.LastCreateObject.OwnerReferences[0].Name)
require.Equal(t, "CosmosFullNode", mClient.LastCreateObject.OwnerReferences[0].Kind)
require.True(t, *mClient.LastCreateObject.OwnerReferences[0].Controller)

mClient.ObjectList = rbacv1.RoleBindingList{Items: []rbacv1.RoleBinding{
{
ObjectMeta: metav1.ObjectMeta{Name: "osmosis-vc-rb", Namespace: crd.Namespace},
Subjects: nil, // different to force update
},
}}

mClient.GotListOpts = nil // reset for next reconcile

err = control.Reconcile(ctx, nopReporter, &crd)
require.NoError(t, err)

require.Len(t, mClient.GotListOpts, 2)
listOpt = client.ListOptions{}
for _, opt := range mClient.GotListOpts {
opt.ApplyToList(&listOpt)
}
require.Equal(t, "test", listOpt.Namespace)
require.Zero(t, listOpt.Limit)

require.Equal(t, 1, mClient.UpdateCount) // Updated 1 role binding.

require.Zero(t, mClient.DeleteCount) // Role bindings are never deleted.
})
}
5 changes: 3 additions & 2 deletions internal/fullnode/role_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func NewRoleControl(client Client) RoleControl {
func (sc RoleControl) Reconcile(ctx context.Context, log kube.Logger, crd *cosmosv1.CosmosFullNode) kube.ReconcileError {
var crs rbacv1.RoleList
if err := sc.client.List(ctx, &crs,
client.InNamespace(crd.Namespace),
client.MatchingLabels{
kube.ControllerLabel: "cosmos-operator",
kube.ComponentLabel: "vc",
Expand All @@ -41,7 +42,7 @@ func (sc RoleControl) Reconcile(ctx context.Context, log kube.Logger, crd *cosmo
diffed := diff.New(current, want)

for _, cr := range diffed.Creates() {
log.Info("Creating role", "roleName", cr.Name)
log.Info("Creating role", "name", cr.Name)
if err := ctrl.SetControllerReference(crd, cr, sc.client.Scheme()); err != nil {
return kube.TransientError(fmt.Errorf("set controller reference on role %q: %w", cr.Name, err))
}
Expand All @@ -53,7 +54,7 @@ func (sc RoleControl) Reconcile(ctx context.Context, log kube.Logger, crd *cosmo
}

for _, cr := range diffed.Updates() {
log.Info("Updating role", "roleName", cr.Name)
log.Info("Updating role", "name", cr.Name)
if err := sc.client.Update(ctx, cr); err != nil {
return kube.TransientError(fmt.Errorf("update role %q: %w", cr.Name, err))
}
Expand Down
70 changes: 70 additions & 0 deletions internal/fullnode/role_control_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package fullnode

import (
"context"
"testing"

"github.com/stretchr/testify/require"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)

func TestRoleControl_Reconcile(t *testing.T) {
t.Parallel()

type mockRoleClient = mockClient[*rbacv1.Role]

ctx := context.Background()

t.Run("happy path", func(t *testing.T) {
crd := defaultCRD()
crd.Namespace = "test"
crd.Spec.Replicas = 3

var mClient mockRoleClient

control := NewRoleControl(&mClient)
err := control.Reconcile(ctx, nopReporter, &crd)
require.NoError(t, err)

require.Len(t, mClient.GotListOpts, 2)
var listOpt client.ListOptions
for _, opt := range mClient.GotListOpts {
opt.ApplyToList(&listOpt)
}
require.Equal(t, "test", listOpt.Namespace)
require.Zero(t, listOpt.Limit)

require.Equal(t, 1, mClient.CreateCount) // Created 1 role.
require.Equal(t, "osmosis-vc-r", mClient.LastCreateObject.Name)
require.NotEmpty(t, mClient.LastCreateObject.OwnerReferences)
require.Equal(t, crd.Name, mClient.LastCreateObject.OwnerReferences[0].Name)
require.Equal(t, "CosmosFullNode", mClient.LastCreateObject.OwnerReferences[0].Kind)
require.True(t, *mClient.LastCreateObject.OwnerReferences[0].Controller)

mClient.ObjectList = rbacv1.RoleList{Items: []rbacv1.Role{
{
ObjectMeta: metav1.ObjectMeta{Name: "osmosis-vc-r", Namespace: crd.Namespace},
Rules: nil, // added to force update
},
}}

mClient.GotListOpts = nil // reset for next reconcile

err = control.Reconcile(ctx, nopReporter, &crd)
require.NoError(t, err)

require.Len(t, mClient.GotListOpts, 2)
listOpt = client.ListOptions{}
for _, opt := range mClient.GotListOpts {
opt.ApplyToList(&listOpt)
}
require.Equal(t, "test", listOpt.Namespace)
require.Zero(t, listOpt.Limit)

require.Equal(t, 1, mClient.UpdateCount) // Updated 1 role.

require.Zero(t, mClient.DeleteCount) // Roles are never deleted.
})
}
26 changes: 13 additions & 13 deletions internal/fullnode/service_account_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ func NewServiceAccountControl(client Client) ServiceAccountControl {

// Reconcile creates or updates service accounts.
func (sc ServiceAccountControl) Reconcile(ctx context.Context, log kube.Logger, crd *cosmosv1.CosmosFullNode) kube.ReconcileError {
var svcs corev1.ServiceAccountList
if err := sc.client.List(ctx, &svcs,
var sas corev1.ServiceAccountList
if err := sc.client.List(ctx, &sas,
client.InNamespace(crd.Namespace),
client.MatchingLabels{
kube.ControllerLabel: "cosmos-operator",
Expand All @@ -37,26 +37,26 @@ func (sc ServiceAccountControl) Reconcile(ctx context.Context, log kube.Logger,
return kube.TransientError(fmt.Errorf("list existing service accounts: %w", err))
}

current := ptrSlice(svcs.Items)
current := ptrSlice(sas.Items)
want := BuildServiceAccounts(crd)
diffed := diff.New(current, want)

for _, svc := range diffed.Creates() {
log.Info("Creating service account", "svcAccountName", svc.Name)
if err := ctrl.SetControllerReference(crd, svc, sc.client.Scheme()); err != nil {
return kube.TransientError(fmt.Errorf("set controller reference on service account %q: %w", svc.Name, err))
for _, sa := range diffed.Creates() {
log.Info("Creating service account", "name", sa.Name)
if err := ctrl.SetControllerReference(crd, sa, sc.client.Scheme()); err != nil {
return kube.TransientError(fmt.Errorf("set controller reference on service account %q: %w", sa.Name, err))
}
// CreateOrUpdate (vs. only create) fixes a bug with current deployments where updating would remove the owner reference.
// This ensures we update the service with the owner reference.
if err := kube.CreateOrUpdate(ctx, sc.client, svc); err != nil {
return kube.TransientError(fmt.Errorf("create service account %q: %w", svc.Name, err))
if err := kube.CreateOrUpdate(ctx, sc.client, sa); err != nil {
return kube.TransientError(fmt.Errorf("create service account %q: %w", sa.Name, err))
}
}

for _, svc := range diffed.Updates() {
log.Info("Updating service account", "svcAccountName", svc.Name)
if err := sc.client.Update(ctx, svc); err != nil {
return kube.TransientError(fmt.Errorf("update service account %q: %w", svc.Name, err))
for _, sa := range diffed.Updates() {
log.Info("Updating service account", "name", sa.Name)
if err := sc.client.Update(ctx, sa); err != nil {
return kube.TransientError(fmt.Errorf("update service account %q: %w", sa.Name, err))
}
}

Expand Down
Loading

0 comments on commit cabd028

Please sign in to comment.