github.com/jenkins-x/jx/v2@v2.1.155/pkg/cmd/controller/controller_role.go (about)

     1  package controller
     2  
     3  import (
     4  	"reflect"
     5  	"time"
     6  
     7  	"github.com/jenkins-x/jx/v2/pkg/errorutil"
     8  
     9  	"github.com/pkg/errors"
    10  
    11  	"github.com/jenkins-x/jx/v2/pkg/cmd/helper"
    12  
    13  	"strings"
    14  
    15  	v1 "github.com/jenkins-x/jx-api/pkg/apis/jenkins.io/v1"
    16  	"github.com/jenkins-x/jx-api/pkg/client/clientset/versioned"
    17  	"github.com/jenkins-x/jx-logging/pkg/log"
    18  	"github.com/jenkins-x/jx/v2/pkg/cmd/opts"
    19  	"github.com/jenkins-x/jx/v2/pkg/cmd/templates"
    20  	"github.com/jenkins-x/jx/v2/pkg/kube"
    21  	"github.com/jenkins-x/jx/v2/pkg/util"
    22  	"github.com/spf13/cobra"
    23  	rbacv1 "k8s.io/api/rbac/v1"
    24  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    25  	"k8s.io/apimachinery/pkg/fields"
    26  	"k8s.io/client-go/kubernetes"
    27  	"k8s.io/client-go/tools/cache"
    28  )
    29  
    30  // ControllerRoleOptions the command line options
    31  type ControllerRoleOptions struct {
    32  	ControllerOptions
    33  
    34  	NoWatch bool
    35  
    36  	Roles           map[string]*rbacv1.Role
    37  	EnvRoleBindings map[string]*v1.EnvironmentRoleBinding
    38  	TeamNs          string
    39  }
    40  
    41  type ControllerRoleFlags struct {
    42  	Version string
    43  }
    44  
    45  const blankSting = ""
    46  
    47  var (
    48  	controllerRoleLong = templates.LongDesc(`
    49  		Controller which replicas Role and EnvironmentRoleBinding resources to Roles and RoleBindings in all matching Environment namespaces. 
    50  
    51  		RBAC in Kubernetes is either global with ClusterRoles or is namespace based with Roles per Namespace.
    52  
    53  		We use a Custom Resource called EnvironmentRoleBinding which binds Roles and its bindings from the development 
    54  		environment into each Environment's Namespace. 
    55  
    56  		e.g. each EnvironmentRoleBinding will result in a RoleBinding and Role resource being create in each matching Environment. 
    57  		So when a Preview environment is created it will have the correct Role and RoleBinding resources added. 
    58  
    59  `)
    60  
    61  	controllerRoleExample = templates.Examples(`
    62  		# watch for changes in Role and EnvironmentRoleBindings in the dev namespace
    63  		# and update the Role + RoleBinding resources in each environment namespace 
    64  		jx controller role
    65  
    66  		# update the current RoleBinding resources in each environment based on the current EnvironmentRoleBindings
    67  		jx controller role --no-watch
    68  
    69  `)
    70  )
    71  
    72  func NewCmdControllerRole(commonOpts *opts.CommonOptions) *cobra.Command {
    73  	options := ControllerRoleOptions{
    74  		ControllerOptions: ControllerOptions{
    75  			CommonOptions: commonOpts,
    76  		},
    77  	}
    78  	cmd := &cobra.Command{
    79  		Use:     "role",
    80  		Short:   "Controller which mirrors Role & EnvironmentRoleBinding resources to Roles and RoleBindings in all matching Environment namespaces",
    81  		Long:    controllerRoleLong,
    82  		Example: controllerRoleExample,
    83  		Run: func(cmd *cobra.Command, args []string) {
    84  			options.Cmd = cmd
    85  			options.Args = args
    86  			err := options.Run()
    87  			helper.CheckErr(err)
    88  		},
    89  	}
    90  
    91  	cmd.Flags().BoolVarP(&options.NoWatch, "no-watch", "n", false, "To disable watching of the resources - to enable one-shot mode")
    92  	return cmd
    93  }
    94  
    95  func (o *ControllerRoleOptions) Run() error {
    96  	jxClient, ns, err := o.JXClientAndDevNamespace()
    97  	if err != nil {
    98  		return err
    99  	}
   100  
   101  	o.TeamNs = ns
   102  	kubeClient, err := o.KubeClient()
   103  	if err != nil {
   104  		return err
   105  	}
   106  
   107  	if !o.NoWatch {
   108  		err = o.WatchRoles(kubeClient, ns)
   109  		if err != nil {
   110  			return err
   111  		}
   112  		err = o.WatchEnvironmentRoleBindings(jxClient, ns)
   113  		if err != nil {
   114  			return err
   115  		}
   116  		err = o.WatchEnvironments(kubeClient, jxClient, ns)
   117  		if err != nil {
   118  			return err
   119  		}
   120  	}
   121  
   122  	roles, err := kubeClient.RbacV1().Roles(ns).List(metav1.ListOptions{})
   123  	if err != nil {
   124  		return err
   125  	}
   126  	for _, role := range roles.Items {
   127  		r := role
   128  		err = o.UpsertRole(&r)
   129  		if err != nil {
   130  			return errors.Wrap(err, "upserting role")
   131  		}
   132  	}
   133  	bindings, err := jxClient.JenkinsV1().EnvironmentRoleBindings(ns).List(metav1.ListOptions{})
   134  	if err != nil {
   135  		return err
   136  	}
   137  	for i := range bindings.Items {
   138  		err = o.UpsertEnvironmentRoleBinding(&bindings.Items[i])
   139  		if err != nil {
   140  			return errors.Wrap(err, "upsert environment role binding resource")
   141  		}
   142  	}
   143  	envList, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   144  	if err != nil {
   145  		return err
   146  	}
   147  	for _, env := range envList.Items {
   148  		environment := env
   149  		err = o.upsertEnvironment(kubeClient, ns, &environment)
   150  		if err != nil {
   151  			return err
   152  		}
   153  	}
   154  	o.upsertRoleIntoEnvRole(ns, jxClient)
   155  	return nil
   156  }
   157  
   158  func (o *ControllerRoleOptions) WatchRoles(kubeClient kubernetes.Interface, ns string) error {
   159  	role := &rbacv1.Role{}
   160  	listWatch := cache.NewListWatchFromClient(kubeClient.RbacV1().RESTClient(), "roles", ns, fields.Everything())
   161  	kube.SortListWatchByName(listWatch)
   162  	_, controller := cache.NewInformer(
   163  		listWatch,
   164  		role,
   165  		time.Minute*10,
   166  		cache.ResourceEventHandlerFuncs{
   167  			AddFunc: func(obj interface{}) {
   168  				o.onRole(nil, obj)
   169  			},
   170  			UpdateFunc: func(oldObj, newObj interface{}) {
   171  				o.onRole(oldObj, newObj)
   172  			},
   173  			DeleteFunc: func(obj interface{}) {
   174  				o.onRole(obj, nil)
   175  			},
   176  		},
   177  	)
   178  
   179  	stop := make(chan struct{})
   180  	go controller.Run(stop)
   181  	return nil
   182  }
   183  
   184  func (o *ControllerRoleOptions) WatchEnvironmentRoleBindings(jxClient versioned.Interface, ns string) error {
   185  	environmentRoleBinding := &v1.EnvironmentRoleBinding{}
   186  	listWatch := cache.NewListWatchFromClient(jxClient.JenkinsV1().RESTClient(), "environmentrolebindings", ns, fields.Everything())
   187  	kube.SortListWatchByName(listWatch)
   188  	_, controller := cache.NewInformer(
   189  		listWatch,
   190  		environmentRoleBinding,
   191  		time.Minute*10,
   192  		cache.ResourceEventHandlerFuncs{
   193  			AddFunc: func(obj interface{}) {
   194  				o.onEnvironmentRoleBinding(nil, obj)
   195  			},
   196  			UpdateFunc: func(oldObj, newObj interface{}) {
   197  				o.onEnvironmentRoleBinding(oldObj, newObj)
   198  			},
   199  			DeleteFunc: func(obj interface{}) {
   200  				o.onEnvironmentRoleBinding(obj, nil)
   201  			},
   202  		},
   203  	)
   204  
   205  	stop := make(chan struct{})
   206  	go controller.Run(stop)
   207  	return nil
   208  }
   209  
   210  func (o *ControllerRoleOptions) WatchEnvironments(kubeClient kubernetes.Interface, jxClient versioned.Interface, ns string) error {
   211  	environment := &v1.Environment{}
   212  	listWatch := cache.NewListWatchFromClient(jxClient.JenkinsV1().RESTClient(), "environments", ns, fields.Everything())
   213  	kube.SortListWatchByName(listWatch)
   214  	_, controller := cache.NewInformer(
   215  		listWatch,
   216  		environment,
   217  		time.Minute*10,
   218  		cache.ResourceEventHandlerFuncs{
   219  			AddFunc: func(obj interface{}) {
   220  				o.onEnvironment(kubeClient, ns, nil, obj)
   221  			},
   222  			UpdateFunc: func(oldObj, newObj interface{}) {
   223  				o.onEnvironment(kubeClient, ns, oldObj, newObj)
   224  			},
   225  			DeleteFunc: func(obj interface{}) {
   226  				o.onEnvironment(kubeClient, ns, obj, nil)
   227  			},
   228  		},
   229  	)
   230  
   231  	stop := make(chan struct{})
   232  	go controller.Run(stop)
   233  
   234  	// Wait forever
   235  	select {}
   236  }
   237  
   238  func (o *ControllerRoleOptions) onEnvironment(kubeClient kubernetes.Interface, ns string, oldObj interface{}, newObj interface{}) {
   239  	var newEnv *v1.Environment
   240  	if newObj != nil {
   241  		newEnv = newObj.(*v1.Environment)
   242  	}
   243  	if oldObj != nil {
   244  		oldEnv := oldObj.(*v1.Environment)
   245  		if oldEnv != nil {
   246  			if newEnv == nil || newEnv.Spec.Namespace != oldEnv.Spec.Namespace {
   247  				err := o.removeEnvironment(kubeClient, ns, oldEnv)
   248  				if err != nil {
   249  					log.Logger().Warnf("Failed to remove role bindings for environment %s: %s", oldEnv.Name, err)
   250  				}
   251  			}
   252  		}
   253  	}
   254  	if newEnv != nil {
   255  		err := o.upsertEnvironment(kubeClient, ns, newEnv)
   256  		if err != nil {
   257  			log.Logger().Warnf("Failed to upsert role bindings for environment %s: %s", newEnv.Name, err)
   258  		}
   259  	}
   260  }
   261  
   262  func (o *ControllerRoleOptions) upsertEnvironment(kubeClient kubernetes.Interface, teamNs string, env *v1.Environment) error {
   263  	errors := []error{}
   264  	ns := env.Spec.Namespace
   265  	if ns != "" {
   266  		for _, binding := range o.EnvRoleBindings {
   267  			err := o.upsertEnvironmentRoleBindingRolesInEnvironments(env, binding, teamNs, ns, kubeClient)
   268  			if err != nil {
   269  				errors = append(errors, err)
   270  			}
   271  
   272  		}
   273  	}
   274  	return errorutil.CombineErrors(errors...)
   275  }
   276  
   277  // upsertEnvironmentRoleBindingRolesInEnvironments for the given environment and environment role binding lets update any role or role bindings if required
   278  func (o *ControllerRoleOptions) upsertEnvironmentRoleBindingRolesInEnvironments(env *v1.Environment, binding *v1.EnvironmentRoleBinding, teamNs string, ns string, kubeClient kubernetes.Interface) error {
   279  	errors := []error{}
   280  	if kube.EnvironmentMatchesAny(env, binding.Spec.Environments) {
   281  		var err error
   282  		if ns != teamNs {
   283  			roleName := binding.Spec.RoleRef.Name
   284  			role := o.Roles[roleName]
   285  			if role == nil {
   286  				log.Logger().Warnf("Cannot find role %s in namespace %s", roleName, teamNs)
   287  			} else {
   288  				roles := kubeClient.RbacV1().Roles(ns)
   289  				var oldRole *rbacv1.Role
   290  				oldRole, err = roles.Get(roleName, metav1.GetOptions{})
   291  				if err == nil && oldRole != nil {
   292  					// lets update it
   293  					changed := false
   294  					if !reflect.DeepEqual(oldRole.Rules, role.Rules) {
   295  						oldRole.Rules = role.Rules
   296  						changed = true
   297  					}
   298  					if changed {
   299  						log.Logger().Infof("Updating Role %s in namespace %s", roleName, ns)
   300  						_, err = roles.Update(oldRole)
   301  					}
   302  				} else {
   303  					log.Logger().Infof("Creating Role %s in namespace %s", roleName, ns)
   304  					newRole := &rbacv1.Role{
   305  						ObjectMeta: metav1.ObjectMeta{
   306  							Name: roleName,
   307  							Labels: map[string]string{
   308  								kube.LabelCreatedBy: kube.ValueCreatedByJX,
   309  								kube.LabelTeam:      teamNs,
   310  							},
   311  						},
   312  						Rules: role.Rules,
   313  					}
   314  					_, err = roles.Create(newRole)
   315  				}
   316  			}
   317  		}
   318  		if err != nil {
   319  			log.Logger().Warnf("Failed: %s", err)
   320  			errors = append(errors, err)
   321  		}
   322  
   323  		bindingName := binding.Name
   324  		roleBindings := kubeClient.RbacV1().RoleBindings(ns)
   325  		var old *rbacv1.RoleBinding
   326  		old, err = roleBindings.Get(bindingName, metav1.GetOptions{})
   327  		if err == nil && old != nil {
   328  			// lets update it
   329  			changed := false
   330  
   331  			if !reflect.DeepEqual(old.RoleRef, binding.Spec.RoleRef) {
   332  				old.RoleRef = binding.Spec.RoleRef
   333  				changed = true
   334  			}
   335  			if !reflect.DeepEqual(old.Subjects, binding.Spec.Subjects) {
   336  				old.Subjects = binding.Spec.Subjects
   337  				changed = true
   338  			}
   339  			if changed {
   340  				log.Logger().Infof("Updating RoleBinding %s in namespace %s", bindingName, ns)
   341  				_, err = roleBindings.Update(old)
   342  			}
   343  		} else {
   344  			log.Logger().Infof("Creating RoleBinding %s in namespace %s", bindingName, ns)
   345  			newBinding := &rbacv1.RoleBinding{
   346  				ObjectMeta: metav1.ObjectMeta{
   347  					Name: bindingName,
   348  					Labels: map[string]string{
   349  						kube.LabelCreatedBy: kube.ValueCreatedByJX,
   350  						kube.LabelTeam:      teamNs,
   351  					},
   352  				},
   353  				Subjects: binding.Spec.Subjects,
   354  				RoleRef:  binding.Spec.RoleRef,
   355  			}
   356  			_, err = roleBindings.Create(newBinding)
   357  		}
   358  		if err != nil {
   359  			log.Logger().Warnf("Failed: %s", err)
   360  			errors = append(errors, err)
   361  		}
   362  	}
   363  	return errorutil.CombineErrors(errors...)
   364  }
   365  
   366  func (o *ControllerRoleOptions) removeEnvironment(kubeClient kubernetes.Interface, curNs string, env *v1.Environment) error {
   367  	ns := env.Spec.Namespace
   368  	if ns != "" {
   369  		for _, binding := range o.EnvRoleBindings {
   370  			if kube.EnvironmentMatchesAny(env, binding.Spec.Environments) {
   371  				// ignore errors
   372  				// ToDo: Evaluate why error is being ignored here
   373  				kubeClient.RbacV1().RoleBindings(ns).Delete(binding.Name, nil) //nolint:errcheck
   374  			}
   375  		}
   376  	}
   377  	return nil
   378  }
   379  
   380  func (o *ControllerRoleOptions) onEnvironmentRoleBinding(oldObj interface{}, newObj interface{}) {
   381  	if o.EnvRoleBindings == nil {
   382  		o.EnvRoleBindings = map[string]*v1.EnvironmentRoleBinding{}
   383  	}
   384  	if oldObj != nil {
   385  		oldEnv := oldObj.(*v1.EnvironmentRoleBinding)
   386  		if oldEnv != nil {
   387  			delete(o.EnvRoleBindings, oldEnv.Name)
   388  		}
   389  	}
   390  	if newObj != nil {
   391  		newEnv := newObj.(*v1.EnvironmentRoleBinding)
   392  		o.UpsertEnvironmentRoleBinding(newEnv) //nolint:errcheck
   393  	}
   394  }
   395  
   396  // UpsertEnvironmentRoleBinding processes an insert/update of the EnvironmentRoleBinding resource
   397  // its public so that we can make testing easier
   398  func (o *ControllerRoleOptions) UpsertEnvironmentRoleBinding(newEnv *v1.EnvironmentRoleBinding) error {
   399  	if newEnv != nil {
   400  		if o.EnvRoleBindings == nil {
   401  			o.EnvRoleBindings = map[string]*v1.EnvironmentRoleBinding{}
   402  		}
   403  		o.EnvRoleBindings[newEnv.Name] = newEnv
   404  	}
   405  
   406  	ns := o.TeamNs
   407  	kubeClient, err := o.KubeClient()
   408  	if err != nil {
   409  		return err
   410  	}
   411  	jxClient, _, err := o.JXClient()
   412  	if err != nil {
   413  		return err
   414  	}
   415  
   416  	// now lets update any roles in any environment we may need to change
   417  	envList, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   418  	if err != nil {
   419  		return err
   420  	}
   421  
   422  	errors := []error{}
   423  	for _, env := range envList.Items {
   424  		environment := env
   425  		err = o.upsertEnvironmentRoleBindingRolesInEnvironments(&environment, newEnv, ns, environment.Spec.Namespace, kubeClient)
   426  		if err != nil {
   427  			errors = append(errors, err)
   428  		}
   429  	}
   430  	return errorutil.CombineErrors(errors...)
   431  }
   432  
   433  func (o *ControllerRoleOptions) onRole(oldObj interface{}, newObj interface{}) {
   434  	if o.Roles == nil {
   435  		o.Roles = map[string]*rbacv1.Role{}
   436  	}
   437  	if oldObj != nil {
   438  		oldRole := oldObj.(*rbacv1.Role)
   439  		if oldRole != nil {
   440  			delete(o.Roles, oldRole.Name)
   441  		}
   442  	}
   443  	if newObj != nil {
   444  		newRole := newObj.(*rbacv1.Role)
   445  		o.UpsertRole(newRole) //nolint:errcheck
   446  	}
   447  }
   448  
   449  // UpsertRole processes the insert/update of a Role
   450  // this function is public for easier testing
   451  func (o *ControllerRoleOptions) UpsertRole(newRole *rbacv1.Role) error {
   452  	if newRole == nil {
   453  		return nil
   454  	}
   455  	if o.Roles == nil {
   456  		o.Roles = map[string]*rbacv1.Role{}
   457  	}
   458  	o.Roles[newRole.Name] = newRole
   459  
   460  	if newRole.Labels == nil || newRole.Labels[kube.LabelKind] != kube.ValueKindEnvironmentRole {
   461  		return nil
   462  	}
   463  
   464  	ns := o.TeamNs
   465  	kubeClient, err := o.KubeClient()
   466  	if err != nil {
   467  		return err
   468  	}
   469  	jxClient, _, err := o.JXClient()
   470  	if err != nil {
   471  		return err
   472  	}
   473  
   474  	// now lets update any roles in any environment we may need to change
   475  	envList, err := jxClient.JenkinsV1().Environments(ns).List(metav1.ListOptions{})
   476  	if err != nil {
   477  		return err
   478  	}
   479  
   480  	errors := []error{}
   481  	for _, env := range envList.Items {
   482  		environment := env
   483  		err = o.upsertRoleInEnvironments(&environment, newRole, ns, environment.Spec.Namespace, kubeClient)
   484  		if err != nil {
   485  			errors = append(errors, err)
   486  		}
   487  	}
   488  	return errorutil.CombineErrors(errors...)
   489  }
   490  
   491  // upsertRoleInEnvironments updates the Role in the team environment in the other environment namespaces if it has changed
   492  func (o *ControllerRoleOptions) upsertRoleInEnvironments(env *v1.Environment, role *rbacv1.Role, teamNs string, ns string, kubeClient kubernetes.Interface) error {
   493  	if ns == teamNs {
   494  		return nil
   495  	}
   496  	var err error
   497  	roleName := role.Name
   498  	roles := kubeClient.RbacV1().Roles(ns)
   499  	var oldRole *rbacv1.Role
   500  	oldRole, err = roles.Get(roleName, metav1.GetOptions{})
   501  	if err == nil && oldRole != nil {
   502  		// lets update it
   503  		changed := false
   504  		if !reflect.DeepEqual(oldRole.Rules, role.Rules) {
   505  			oldRole.Rules = role.Rules
   506  			changed = true
   507  		}
   508  		if changed {
   509  			log.Logger().Infof("Updating Role %s in namespace %s", roleName, ns)
   510  			_, err = roles.Update(oldRole)
   511  		}
   512  	} else {
   513  		log.Logger().Infof("Creating Role %s in namespace %s", roleName, ns)
   514  		newRole := &rbacv1.Role{
   515  			ObjectMeta: metav1.ObjectMeta{
   516  				Name: roleName,
   517  				Labels: map[string]string{
   518  					kube.LabelCreatedBy: kube.ValueCreatedByJX,
   519  					kube.LabelTeam:      teamNs,
   520  				},
   521  			},
   522  			Rules: role.Rules,
   523  		}
   524  		_, err = roles.Create(newRole)
   525  	}
   526  	return err
   527  }
   528  
   529  func (o *ControllerRoleOptions) upsertRoleIntoEnvRole(ns string, jxClient versioned.Interface) {
   530  	foundRole := 0
   531  	for _, roleValue := range o.Roles {
   532  		for labelK, labelV := range roleValue.Labels {
   533  			if util.StringMatchesPattern(labelK, kube.LabelKind) && util.StringMatchesPattern(labelV, kube.ValueKindEnvironmentRole) {
   534  				for _, envRoleValue := range o.EnvRoleBindings {
   535  					if util.StringMatchesPattern(strings.Trim(roleValue.GetName(), blankSting), strings.Trim(envRoleValue.Spec.RoleRef.Name, blankSting)) {
   536  						foundRole = 1
   537  						break
   538  					}
   539  				}
   540  				if foundRole == 0 {
   541  					log.Logger().Infof("Environment binding doesn't exist for role %s , creating it.", util.ColorInfo(roleValue.GetName()))
   542  					newSubject := rbacv1.Subject{
   543  						Name:      roleValue.GetName(),
   544  						Kind:      kube.ValueKindEnvironmentRole,
   545  						Namespace: ns,
   546  					}
   547  					newEnvRoleBinding := &v1.EnvironmentRoleBinding{
   548  						ObjectMeta: metav1.ObjectMeta{
   549  							Name:      roleValue.GetName(),
   550  							Namespace: ns,
   551  						},
   552  						Spec: v1.EnvironmentRoleBindingSpec{
   553  							Subjects: []rbacv1.Subject{
   554  								newSubject,
   555  							},
   556  						},
   557  					}
   558  					jxClient.JenkinsV1().EnvironmentRoleBindings(ns).Create(newEnvRoleBinding) //nolint:errcheck
   559  				}
   560  			}
   561  		}
   562  
   563  	}
   564  }