#200 Allow for additional RBAC role bindings for Jenkins master
This commit is contained in:
		
							parent
							
								
									9b5672ebcd
								
							
						
					
					
						commit
						f9335df74c
					
				|  | @ -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
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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) | ||||
|  |  | |||
|  | @ -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) | ||||
| 	}) | ||||
| } | ||||
|  |  | |||
|  | @ -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, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
|  |  | |||
|  | @ -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", | ||||
| 				}, | ||||
| 			}, | ||||
| 		}, | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue