...

Source file src/k8s.io/kubectl/pkg/cmd/auth/reconcile.go

Documentation: k8s.io/kubectl/pkg/cmd/auth

     1  /*
     2  Copyright 2017 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package auth
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  
    23  	"github.com/spf13/cobra"
    24  	"k8s.io/apimachinery/pkg/runtime"
    25  	"k8s.io/klog/v2"
    26  
    27  	rbacv1 "k8s.io/api/rbac/v1"
    28  	rbacv1alpha1 "k8s.io/api/rbac/v1alpha1"
    29  	rbacv1beta1 "k8s.io/api/rbac/v1beta1"
    30  	"k8s.io/cli-runtime/pkg/genericclioptions"
    31  	"k8s.io/cli-runtime/pkg/genericiooptions"
    32  	"k8s.io/cli-runtime/pkg/printers"
    33  	"k8s.io/cli-runtime/pkg/resource"
    34  	corev1client "k8s.io/client-go/kubernetes/typed/core/v1"
    35  	rbacv1client "k8s.io/client-go/kubernetes/typed/rbac/v1"
    36  	"k8s.io/component-helpers/auth/rbac/reconciliation"
    37  	cmdutil "k8s.io/kubectl/pkg/cmd/util"
    38  	"k8s.io/kubectl/pkg/scheme"
    39  	"k8s.io/kubectl/pkg/util/templates"
    40  )
    41  
    42  // ReconcileOptions is the start of the data required to perform the operation.  As new fields are added, add them here instead of
    43  // referencing the cmd.Flags()
    44  type ReconcileOptions struct {
    45  	PrintFlags             *genericclioptions.PrintFlags
    46  	FilenameOptions        *resource.FilenameOptions
    47  	DryRun                 bool
    48  	RemoveExtraPermissions bool
    49  	RemoveExtraSubjects    bool
    50  
    51  	Visitor         resource.Visitor
    52  	RBACClient      rbacv1client.RbacV1Interface
    53  	NamespaceClient corev1client.CoreV1Interface
    54  
    55  	PrintObject printers.ResourcePrinterFunc
    56  
    57  	genericiooptions.IOStreams
    58  }
    59  
    60  var (
    61  	reconcileLong = templates.LongDesc(`
    62  		Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects.
    63  
    64  		Missing objects are created, and the containing namespace is created for namespaced objects, if required.
    65  
    66  		Existing roles are updated to include the permissions in the input objects,
    67  		and remove extra permissions if --remove-extra-permissions is specified.
    68  
    69  		Existing bindings are updated to include the subjects in the input objects,
    70  		and remove extra subjects if --remove-extra-subjects is specified.
    71  
    72  		This is preferred to 'apply' for RBAC resources so that semantically-aware merging of rules and subjects is done.`)
    73  
    74  	reconcileExample = templates.Examples(`
    75  		# Reconcile RBAC resources from a file
    76  		kubectl auth reconcile -f my-rbac-rules.yaml`)
    77  )
    78  
    79  // NewReconcileOptions returns a new ReconcileOptions instance
    80  func NewReconcileOptions(ioStreams genericiooptions.IOStreams) *ReconcileOptions {
    81  	return &ReconcileOptions{
    82  		FilenameOptions: &resource.FilenameOptions{},
    83  		PrintFlags:      genericclioptions.NewPrintFlags("reconciled").WithTypeSetter(scheme.Scheme),
    84  		IOStreams:       ioStreams,
    85  	}
    86  }
    87  
    88  // NewCmdReconcile holds the options for 'auth reconcile' sub command
    89  func NewCmdReconcile(f cmdutil.Factory, streams genericiooptions.IOStreams) *cobra.Command {
    90  	o := NewReconcileOptions(streams)
    91  
    92  	cmd := &cobra.Command{
    93  		Use:                   "reconcile -f FILENAME",
    94  		DisableFlagsInUseLine: true,
    95  		Short:                 "Reconciles rules for RBAC role, role binding, cluster role, and cluster role binding objects",
    96  		Long:                  reconcileLong,
    97  		Example:               reconcileExample,
    98  		Run: func(cmd *cobra.Command, args []string) {
    99  			cmdutil.CheckErr(o.Complete(cmd, f, args))
   100  			cmdutil.CheckErr(o.Validate())
   101  			cmdutil.CheckErr(o.RunReconcile())
   102  		},
   103  	}
   104  
   105  	o.PrintFlags.AddFlags(cmd)
   106  
   107  	cmdutil.AddFilenameOptionFlags(cmd, o.FilenameOptions, "identifying the resource to reconcile.")
   108  	cmd.Flags().BoolVar(&o.RemoveExtraPermissions, "remove-extra-permissions", o.RemoveExtraPermissions, "If true, removes extra permissions added to roles")
   109  	cmd.Flags().BoolVar(&o.RemoveExtraSubjects, "remove-extra-subjects", o.RemoveExtraSubjects, "If true, removes extra subjects added to rolebindings")
   110  	cmdutil.AddDryRunFlag(cmd)
   111  
   112  	return cmd
   113  }
   114  
   115  // Complete completes all the required options
   116  func (o *ReconcileOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string) error {
   117  	if err := o.FilenameOptions.RequireFilenameOrKustomize(); err != nil {
   118  		return err
   119  	}
   120  
   121  	if len(args) > 0 {
   122  		return errors.New("no arguments are allowed")
   123  	}
   124  
   125  	dryRun, err := getClientSideDryRun(cmd)
   126  	if err != nil {
   127  		return err
   128  	}
   129  	o.DryRun = dryRun
   130  
   131  	namespace, enforceNamespace, err := f.ToRawKubeConfigLoader().Namespace()
   132  	if err != nil {
   133  		return err
   134  	}
   135  
   136  	r := f.NewBuilder().
   137  		WithScheme(scheme.Scheme, scheme.Scheme.PrioritizedVersionsAllGroups()...).
   138  		ContinueOnError().
   139  		NamespaceParam(namespace).DefaultNamespace().
   140  		FilenameParam(enforceNamespace, o.FilenameOptions).
   141  		Flatten().
   142  		Local().
   143  		Do()
   144  
   145  	if err := r.Err(); err != nil {
   146  		return err
   147  	}
   148  	o.Visitor = r
   149  
   150  	clientConfig, err := f.ToRESTConfig()
   151  	if err != nil {
   152  		return err
   153  	}
   154  	o.RBACClient, err = rbacv1client.NewForConfig(clientConfig)
   155  	if err != nil {
   156  		return err
   157  	}
   158  	o.NamespaceClient, err = corev1client.NewForConfig(clientConfig)
   159  	if err != nil {
   160  		return err
   161  	}
   162  
   163  	if o.DryRun {
   164  		o.PrintFlags.Complete("%s (dry run)")
   165  	}
   166  	printer, err := o.PrintFlags.ToPrinter()
   167  	if err != nil {
   168  		return err
   169  	}
   170  
   171  	o.PrintObject = printer.PrintObj
   172  	return nil
   173  }
   174  
   175  // Validate makes sure provided values for ReconcileOptions are valid
   176  func (o *ReconcileOptions) Validate() error {
   177  	if o.Visitor == nil {
   178  		return errors.New("ReconcileOptions.Visitor must be set")
   179  	}
   180  	if o.RBACClient == nil {
   181  		return errors.New("ReconcileOptions.RBACClient must be set")
   182  	}
   183  	if o.NamespaceClient == nil {
   184  		return errors.New("ReconcileOptions.NamespaceClient must be set")
   185  	}
   186  	if o.PrintObject == nil {
   187  		return errors.New("ReconcileOptions.Print must be set")
   188  	}
   189  	if o.Out == nil {
   190  		return errors.New("ReconcileOptions.Out must be set")
   191  	}
   192  	if o.ErrOut == nil {
   193  		return errors.New("ReconcileOptions.Err must be set")
   194  	}
   195  	return nil
   196  }
   197  
   198  // RunReconcile performs the execution
   199  func (o *ReconcileOptions) RunReconcile() error {
   200  	return o.Visitor.Visit(func(info *resource.Info, err error) error {
   201  		if err != nil {
   202  			return err
   203  		}
   204  
   205  		switch t := info.Object.(type) {
   206  		case *rbacv1.Role:
   207  			reconcileOptions := reconciliation.ReconcileRoleOptions{
   208  				Confirm:                !o.DryRun,
   209  				RemoveExtraPermissions: o.RemoveExtraPermissions,
   210  				Role:                   reconciliation.RoleRuleOwner{Role: t},
   211  				Client: reconciliation.RoleModifier{
   212  					NamespaceClient: o.NamespaceClient.Namespaces(),
   213  					Client:          o.RBACClient,
   214  				},
   215  			}
   216  			result, err := reconcileOptions.Run()
   217  			if err != nil {
   218  				return err
   219  			}
   220  			o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected)
   221  
   222  		case *rbacv1.ClusterRole:
   223  			reconcileOptions := reconciliation.ReconcileRoleOptions{
   224  				Confirm:                !o.DryRun,
   225  				RemoveExtraPermissions: o.RemoveExtraPermissions,
   226  				Role:                   reconciliation.ClusterRoleRuleOwner{ClusterRole: t},
   227  				Client: reconciliation.ClusterRoleModifier{
   228  					Client: o.RBACClient.ClusterRoles(),
   229  				},
   230  			}
   231  			result, err := reconcileOptions.Run()
   232  			if err != nil {
   233  				return err
   234  			}
   235  			o.printResults(result.Role.GetObject(), nil, nil, result.MissingRules, result.ExtraRules, result.Operation, result.Protected)
   236  
   237  		case *rbacv1.RoleBinding:
   238  			reconcileOptions := reconciliation.ReconcileRoleBindingOptions{
   239  				Confirm:             !o.DryRun,
   240  				RemoveExtraSubjects: o.RemoveExtraSubjects,
   241  				RoleBinding:         reconciliation.RoleBindingAdapter{RoleBinding: t},
   242  				Client: reconciliation.RoleBindingClientAdapter{
   243  					Client:          o.RBACClient,
   244  					NamespaceClient: o.NamespaceClient.Namespaces(),
   245  				},
   246  			}
   247  			result, err := reconcileOptions.Run()
   248  			if err != nil {
   249  				return err
   250  			}
   251  			o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
   252  
   253  		case *rbacv1.ClusterRoleBinding:
   254  			reconcileOptions := reconciliation.ReconcileRoleBindingOptions{
   255  				Confirm:             !o.DryRun,
   256  				RemoveExtraSubjects: o.RemoveExtraSubjects,
   257  				RoleBinding:         reconciliation.ClusterRoleBindingAdapter{ClusterRoleBinding: t},
   258  				Client: reconciliation.ClusterRoleBindingClientAdapter{
   259  					Client: o.RBACClient.ClusterRoleBindings(),
   260  				},
   261  			}
   262  			result, err := reconcileOptions.Run()
   263  			if err != nil {
   264  				return err
   265  			}
   266  			o.printResults(result.RoleBinding.GetObject(), result.MissingSubjects, result.ExtraSubjects, nil, nil, result.Operation, result.Protected)
   267  
   268  		case *rbacv1beta1.Role,
   269  			*rbacv1beta1.RoleBinding,
   270  			*rbacv1beta1.ClusterRole,
   271  			*rbacv1beta1.ClusterRoleBinding,
   272  			*rbacv1alpha1.Role,
   273  			*rbacv1alpha1.RoleBinding,
   274  			*rbacv1alpha1.ClusterRole,
   275  			*rbacv1alpha1.ClusterRoleBinding:
   276  			return fmt.Errorf("only rbac.authorization.k8s.io/v1 is supported: not %T", t)
   277  
   278  		default:
   279  			klog.V(1).Infof("skipping %#v", info.Object.GetObjectKind())
   280  			// skip ignored resources
   281  		}
   282  
   283  		return nil
   284  	})
   285  }
   286  
   287  func (o *ReconcileOptions) printResults(object runtime.Object,
   288  	missingSubjects, extraSubjects []rbacv1.Subject,
   289  	missingRules, extraRules []rbacv1.PolicyRule,
   290  	operation reconciliation.ReconcileOperation,
   291  	protected bool) {
   292  
   293  	o.PrintObject(object, o.Out)
   294  
   295  	caveat := ""
   296  	if protected {
   297  		caveat = ", but object opted out (rbac.authorization.kubernetes.io/autoupdate: false)"
   298  	}
   299  	switch operation {
   300  	case reconciliation.ReconcileNone:
   301  		return
   302  	case reconciliation.ReconcileCreate:
   303  		fmt.Fprintf(o.ErrOut, "\treconciliation required create%s\n", caveat)
   304  	case reconciliation.ReconcileUpdate:
   305  		fmt.Fprintf(o.ErrOut, "\treconciliation required update%s\n", caveat)
   306  	case reconciliation.ReconcileRecreate:
   307  		fmt.Fprintf(o.ErrOut, "\treconciliation required recreate%s\n", caveat)
   308  	}
   309  
   310  	if len(missingSubjects) > 0 {
   311  		fmt.Fprintf(o.ErrOut, "\tmissing subjects added:\n")
   312  		for _, s := range missingSubjects {
   313  			fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s)
   314  		}
   315  	}
   316  	if o.RemoveExtraSubjects {
   317  		if len(extraSubjects) > 0 {
   318  			fmt.Fprintf(o.ErrOut, "\textra subjects removed:\n")
   319  			for _, s := range extraSubjects {
   320  				fmt.Fprintf(o.ErrOut, "\t\t%+v\n", s)
   321  			}
   322  		}
   323  	}
   324  	if len(missingRules) > 0 {
   325  		fmt.Fprintf(o.ErrOut, "\tmissing rules added:\n")
   326  		for _, r := range missingRules {
   327  			fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r)
   328  		}
   329  	}
   330  	if o.RemoveExtraPermissions {
   331  		if len(extraRules) > 0 {
   332  			fmt.Fprintf(o.ErrOut, "\textra rules removed:\n")
   333  			for _, r := range extraRules {
   334  				fmt.Fprintf(o.ErrOut, "\t\t%+v\n", r)
   335  			}
   336  		}
   337  	}
   338  }
   339  
   340  func getClientSideDryRun(cmd *cobra.Command) (bool, error) {
   341  	dryRunStrategy, err := cmdutil.GetDryRunStrategy(cmd)
   342  	if err != nil {
   343  		return false, fmt.Errorf("error accessing --dry-run flag for command %s: %v", cmd.Name(), err)
   344  	}
   345  	if dryRunStrategy == cmdutil.DryRunServer {
   346  		return false, fmt.Errorf("--dry-run=server for command %s is not supported yet", cmd.Name())
   347  	}
   348  	return dryRunStrategy == cmdutil.DryRunClient, nil
   349  }
   350  

View as plain text