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 }