diff --git a/pkg/apis/jenkins/v1alpha2/jenkins_types.go b/pkg/apis/jenkins/v1alpha2/jenkins_types.go index 9fd2f7a1..59c3767c 100644 --- a/pkg/apis/jenkins/v1alpha2/jenkins_types.go +++ b/pkg/apis/jenkins/v1alpha2/jenkins_types.go @@ -2,6 +2,7 @@ package v1alpha2 import ( corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -52,6 +53,10 @@ type JenkinsSpec struct { // ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin // +optional ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` + + // Roles defines list of extra RBAC roles for the Jenkins Master pod service account + // +optional + Roles []rbacv1.RoleRef `json:"roles,omitempty"` } // NotificationLevel defines the level of a Notification diff --git a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go index 7175530d..5a34d171 100644 --- a/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/jenkins/v1alpha2/zz_generated.deepcopy.go @@ -6,6 +6,7 @@ package v1alpha2 import ( v1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -369,6 +370,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) { in.Restore.DeepCopyInto(&out.Restore) in.GroovyScripts.DeepCopyInto(&out.GroovyScripts) in.ConfigurationAsCode.DeepCopyInto(&out.ConfigurationAsCode) + if in.Roles != nil { + in, out := &in.Roles, &out.Roles + *out = make([]rbacv1.RoleRef, len(*in)) + copy(*out, *in) + } return } diff --git a/pkg/controller/jenkins/configuration/base/reconcile.go b/pkg/controller/jenkins/configuration/base/reconcile.go index c236d59c..70b40905 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile.go +++ b/pkg/controller/jenkins/configuration/base/reconcile.go @@ -25,6 +25,7 @@ import ( "github.com/go-logr/logr" stackerr "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" @@ -180,6 +181,11 @@ func (r *ReconcileJenkinsBaseConfiguration) ensureResourcesRequiredForJenkinsPod } r.logger.V(log.VDebug).Info("Service account, role and role binding are present") + if err := r.ensureExtraRBAC(metaObject); err != nil { + return err + } + r.logger.V(log.VDebug).Info("Extra role bindings are present") + if err := r.createService(metaObject, resources.GetJenkinsHTTPServiceName(r.Configuration.Jenkins), r.Configuration.Jenkins.Spec.Service); err != nil { return err } @@ -347,7 +353,11 @@ func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) e return stackerr.WithStack(err) } - roleBinding := resources.NewRoleBinding(meta) + roleBinding := resources.NewRoleBinding(meta.Name, meta.Namespace, meta.Name, rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: meta.Name, + }) err = r.CreateOrUpdateResource(roleBinding) if err != nil { return stackerr.WithStack(err) @@ -356,6 +366,58 @@ func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) e return nil } +func (r *ReconcileJenkinsBaseConfiguration) ensureExtraRBAC(meta metav1.ObjectMeta) error { + var err error + var name string + for _, roleRef := range r.Configuration.Jenkins.Spec.Roles { + name = getExtraRoleBindingName(meta.Name, roleRef) + roleBinding := resources.NewRoleBinding(name, meta.Namespace, meta.Name, roleRef) + err = r.CreateOrUpdateResource(roleBinding) + if err != nil { + return stackerr.WithStack(err) + } + } + + roleBindings := &rbacv1.RoleBindingList{} + err = r.Client.List(context.TODO(), &client.ListOptions{Namespace: r.Configuration.Jenkins.Namespace}, roleBindings) + if err != nil { + return stackerr.WithStack(err) + } + for _, roleBinding := range roleBindings.Items { + if !strings.HasPrefix(roleBinding.Name, getExtraRoleBindingName(meta.Name, rbacv1.RoleRef{Kind: "Role"})) && + !strings.HasPrefix(roleBinding.Name, getExtraRoleBindingName(meta.Name, rbacv1.RoleRef{Kind: "ClusterRole"})) { + continue + } + + found := false + for _, roleRef := range r.Configuration.Jenkins.Spec.Roles { + name = getExtraRoleBindingName(meta.Name, roleRef) + if roleBinding.Name == name { + found = true + continue + } + } + if !found { + r.logger.Info(fmt.Sprintf("Deleting RoleBinding '%s'", roleBinding.Name)) + if err = r.Client.Delete(context.TODO(), &roleBinding); err != nil { + return stackerr.WithStack(err) + } + } + } + + return nil +} + +func getExtraRoleBindingName(serviceAccountName string, roleRef rbacv1.RoleRef) string { + var typeName string + if roleRef.Kind == "ClusterRole" { + typeName = "cr" + } else { + typeName = "r" + } + return fmt.Sprintf("%s-%s-%s", serviceAccountName, typeName, roleRef.Name) +} + func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta, name string, config v1alpha2.Service) error { service := corev1.Service{} err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &service) diff --git a/pkg/controller/jenkins/configuration/base/reconcile_test.go b/pkg/controller/jenkins/configuration/base/reconcile_test.go index 205f3d36..0b3dd066 100644 --- a/pkg/controller/jenkins/configuration/base/reconcile_test.go +++ b/pkg/controller/jenkins/configuration/base/reconcile_test.go @@ -1,6 +1,7 @@ package base import ( + "context" "testing" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" @@ -13,6 +14,11 @@ import ( "github.com/golang/mock/gomock" "github.com/stretchr/testify/assert" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/scheme" + k8sclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestGetJenkinsOpts(t *testing.T) { @@ -733,3 +739,184 @@ func TestCompareImagePullSecrets(t *testing.T) { assert.False(t, got) }) } + +func TestEnsureExtraRBAC(t *testing.T) { + namespace := "default" + jenkinsName := "example" + log.SetupLogger(true) + + fetchAllRoleBindings := func(client k8sclient.Client) (roleBindings *rbacv1.RoleBindingList, err error) { + roleBindings = &rbacv1.RoleBindingList{} + err = client.List(context.TODO(), &k8sclient.ListOptions{Namespace: namespace}, roleBindings) + return + } + + t.Run("empty", func(t *testing.T) { + // given + fakeClient := fake.NewFakeClient() + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + assert.NoError(t, err) + + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + Roles: []rbacv1.RoleRef{}, + }, + } + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + metaObject := resources.NewResourceObjectMeta(jenkins) + + // when + err = reconciler.createRBAC(metaObject) + assert.NoError(t, err) + err = reconciler.ensureExtraRBAC(metaObject) + assert.NoError(t, err) + + // then + roleBindings, err := fetchAllRoleBindings(fakeClient) + assert.NoError(t, err) + assert.Equal(t, 1, len(roleBindings.Items)) + assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name) + }) + clusterRoleKind := "ClusterRole" + t.Run("one extra", func(t *testing.T) { + // given + fakeClient := fake.NewFakeClient() + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + assert.NoError(t, err) + + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + Roles: []rbacv1.RoleRef{ + {APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "edit", + }, + }, + }, + } + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + metaObject := resources.NewResourceObjectMeta(jenkins) + + // when + err = reconciler.createRBAC(metaObject) + assert.NoError(t, err) + err = reconciler.ensureExtraRBAC(metaObject) + assert.NoError(t, err) + + // then + roleBindings, err := fetchAllRoleBindings(fakeClient) + assert.NoError(t, err) + assert.Equal(t, 2, len(roleBindings.Items)) + assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name) + assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[1].RoleRef) + }) + t.Run("two extra", func(t *testing.T) { + // given + fakeClient := fake.NewFakeClient() + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + assert.NoError(t, err) + + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + Roles: []rbacv1.RoleRef{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "admin", + }, + { + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "edit", + }, + }, + }, + } + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, nil, client.JenkinsAPIConnectionSettings{}, nil) + metaObject := resources.NewResourceObjectMeta(jenkins) + + // when + err = reconciler.createRBAC(metaObject) + assert.NoError(t, err) + err = reconciler.ensureExtraRBAC(metaObject) + assert.NoError(t, err) + + // then + roleBindings, err := fetchAllRoleBindings(fakeClient) + assert.NoError(t, err) + assert.Equal(t, 3, len(roleBindings.Items)) + assert.Equal(t, metaObject.Name, roleBindings.Items[0].Name) + assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[1].RoleRef) + assert.Equal(t, jenkins.Spec.Roles[1], roleBindings.Items[2].RoleRef) + }) + t.Run("delete one extra", func(t *testing.T) { + // given + fakeClient := fake.NewFakeClient() + err := v1alpha2.SchemeBuilder.AddToScheme(scheme.Scheme) + assert.NoError(t, err) + + jenkins := &v1alpha2.Jenkins{ + ObjectMeta: metav1.ObjectMeta{ + Name: jenkinsName, + Namespace: namespace, + }, + Spec: v1alpha2.JenkinsSpec{ + Roles: []rbacv1.RoleRef{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "admin", + }, + { + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "edit", + }, + }, + }, + } + reconciler := New(configuration.Configuration{Client: fakeClient, Jenkins: jenkins, Scheme: scheme.Scheme}, log.Log, client.JenkinsAPIConnectionSettings{}, nil) + metaObject := resources.NewResourceObjectMeta(jenkins) + + // when + roleBindingSkipMe := resources.NewRoleBinding("skip-me", namespace, metaObject.Name, rbacv1.RoleRef{ + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "edit", + }) + err = reconciler.CreateOrUpdateResource(roleBindingSkipMe) + assert.NoError(t, err) + err = reconciler.createRBAC(metaObject) + assert.NoError(t, err) + err = reconciler.ensureExtraRBAC(metaObject) + assert.NoError(t, err) + jenkins.Spec.Roles = []rbacv1.RoleRef{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: clusterRoleKind, + Name: "admin", + }, + } + err = reconciler.ensureExtraRBAC(metaObject) + assert.NoError(t, err) + + // then + roleBindings, err := fetchAllRoleBindings(fakeClient) + assert.NoError(t, err) + assert.Equal(t, 3, len(roleBindings.Items)) + assert.Equal(t, metaObject.Name, roleBindings.Items[1].Name) + assert.Equal(t, jenkins.Spec.Roles[0], roleBindings.Items[2].RoleRef) + }) +} diff --git a/pkg/controller/jenkins/configuration/base/resources/rbac.go b/pkg/controller/jenkins/configuration/base/resources/rbac.go index 729db48c..e6e23ca3 100644 --- a/pkg/controller/jenkins/configuration/base/resources/rbac.go +++ b/pkg/controller/jenkins/configuration/base/resources/rbac.go @@ -54,23 +54,22 @@ func NewRole(meta metav1.ObjectMeta) *v1.Role { } // NewRoleBinding returns rbac role binding for jenkins master -func NewRoleBinding(meta metav1.ObjectMeta) *v1.RoleBinding { +func NewRoleBinding(name, namespace, serviceAccountName string, roleRef v1.RoleRef) *v1.RoleBinding { return &v1.RoleBinding{ TypeMeta: metav1.TypeMeta{ Kind: "RoleBinding", APIVersion: "rbac.authorization.k8s.io/v1", }, - ObjectMeta: meta, - RoleRef: v1.RoleRef{ - APIGroup: "rbac.authorization.k8s.io", - Kind: "Role", - Name: meta.Name, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, }, + RoleRef: roleRef, Subjects: []v1.Subject{ { Kind: "ServiceAccount", - Name: meta.Name, - Namespace: meta.Namespace, + Name: serviceAccountName, + Namespace: namespace, }, }, } diff --git a/test/e2e/jenkins.go b/test/e2e/jenkins.go index 66c3df56..a462b4de 100644 --- a/test/e2e/jenkins.go +++ b/test/e2e/jenkins.go @@ -10,6 +10,7 @@ import ( "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" framework "github.com/operator-framework/operator-sdk/pkg/test" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/types" @@ -144,6 +145,13 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S Type: corev1.ServiceTypeNodePort, Port: constants.DefaultHTTPPortInt32, }, + Roles: []rbacv1.RoleRef{ + { + APIGroup: "rbac.authorization.k8s.io", + Kind: "Role", + Name: "view", + }, + }, }, }