#200 Allow for additional RBAC role bindings for Jenkins master

This commit is contained in:
Tomasz Sęk 2020-01-06 18:03:01 +01:00
parent 9b5672ebcd
commit f9335df74c
No known key found for this signature in database
GPG Key ID: DC356D23F6A644D0
6 changed files with 276 additions and 9 deletions

View File

@ -2,6 +2,7 @@ package v1alpha2
import ( import (
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/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 // ConfigurationAsCode defines configuration of Jenkins customization via Configuration as Code Jenkins plugin
// +optional // +optional
ConfigurationAsCode ConfigurationAsCode `json:"configurationAsCode,omitempty"` 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 // NotificationLevel defines the level of a Notification

View File

@ -6,6 +6,7 @@ package v1alpha2
import ( import (
v1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
runtime "k8s.io/apimachinery/pkg/runtime" runtime "k8s.io/apimachinery/pkg/runtime"
) )
@ -369,6 +370,11 @@ func (in *JenkinsSpec) DeepCopyInto(out *JenkinsSpec) {
in.Restore.DeepCopyInto(&out.Restore) in.Restore.DeepCopyInto(&out.Restore)
in.GroovyScripts.DeepCopyInto(&out.GroovyScripts) in.GroovyScripts.DeepCopyInto(&out.GroovyScripts)
in.ConfigurationAsCode.DeepCopyInto(&out.ConfigurationAsCode) 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 return
} }

View File

@ -25,6 +25,7 @@ import (
"github.com/go-logr/logr" "github.com/go-logr/logr"
stackerr "github.com/pkg/errors" stackerr "github.com/pkg/errors"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors" apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types" "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") 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 { if err := r.createService(metaObject, resources.GetJenkinsHTTPServiceName(r.Configuration.Jenkins), r.Configuration.Jenkins.Spec.Service); err != nil {
return err return err
} }
@ -347,7 +353,11 @@ func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) e
return stackerr.WithStack(err) 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) err = r.CreateOrUpdateResource(roleBinding)
if err != nil { if err != nil {
return stackerr.WithStack(err) return stackerr.WithStack(err)
@ -356,6 +366,58 @@ func (r *ReconcileJenkinsBaseConfiguration) createRBAC(meta metav1.ObjectMeta) e
return nil 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 { func (r *ReconcileJenkinsBaseConfiguration) createService(meta metav1.ObjectMeta, name string, config v1alpha2.Service) error {
service := corev1.Service{} service := corev1.Service{}
err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &service) err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name, Namespace: meta.Namespace}, &service)

View File

@ -1,6 +1,7 @@
package base package base
import ( import (
"context"
"testing" "testing"
"github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2" "github.com/jenkinsci/kubernetes-operator/pkg/apis/jenkins/v1alpha2"
@ -13,6 +14,11 @@ import (
"github.com/golang/mock/gomock" "github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
corev1 "k8s.io/api/core/v1" 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) { func TestGetJenkinsOpts(t *testing.T) {
@ -733,3 +739,184 @@ func TestCompareImagePullSecrets(t *testing.T) {
assert.False(t, got) 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)
})
}

View File

@ -54,23 +54,22 @@ func NewRole(meta metav1.ObjectMeta) *v1.Role {
} }
// NewRoleBinding returns rbac role binding for jenkins master // 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{ return &v1.RoleBinding{
TypeMeta: metav1.TypeMeta{ TypeMeta: metav1.TypeMeta{
Kind: "RoleBinding", Kind: "RoleBinding",
APIVersion: "rbac.authorization.k8s.io/v1", APIVersion: "rbac.authorization.k8s.io/v1",
}, },
ObjectMeta: meta, ObjectMeta: metav1.ObjectMeta{
RoleRef: v1.RoleRef{ Name: name,
APIGroup: "rbac.authorization.k8s.io", Namespace: namespace,
Kind: "Role",
Name: meta.Name,
}, },
RoleRef: roleRef,
Subjects: []v1.Subject{ Subjects: []v1.Subject{
{ {
Kind: "ServiceAccount", Kind: "ServiceAccount",
Name: meta.Name, Name: serviceAccountName,
Namespace: meta.Namespace, Namespace: namespace,
}, },
}, },
} }

View File

@ -10,6 +10,7 @@ import (
"github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants" "github.com/jenkinsci/kubernetes-operator/pkg/controller/jenkins/constants"
framework "github.com/operator-framework/operator-sdk/pkg/test" framework "github.com/operator-framework/operator-sdk/pkg/test"
corev1 "k8s.io/api/core/v1" corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/types"
@ -144,6 +145,13 @@ func createJenkinsCR(t *testing.T, name, namespace string, seedJob *[]v1alpha2.S
Type: corev1.ServiceTypeNodePort, Type: corev1.ServiceTypeNodePort,
Port: constants.DefaultHTTPPortInt32, Port: constants.DefaultHTTPPortInt32,
}, },
Roles: []rbacv1.RoleRef{
{
APIGroup: "rbac.authorization.k8s.io",
Kind: "Role",
Name: "view",
},
},
}, },
} }