diff --git a/docs/.generated_docs b/docs/.generated_docs index 6d85ea2ebe..6d8f6385c6 100644 --- a/docs/.generated_docs +++ b/docs/.generated_docs @@ -27,6 +27,7 @@ docs/man/man1/kubectl-apply-view-last-applied.1 docs/man/man1/kubectl-apply.1 docs/man/man1/kubectl-attach.1 docs/man/man1/kubectl-auth-can-i.1 +docs/man/man1/kubectl-auth-reconcile.1 docs/man/man1/kubectl-auth.1 docs/man/man1/kubectl-autoscale.1 docs/man/man1/kubectl-certificate-approve.1 @@ -121,6 +122,7 @@ docs/user-guide/kubectl/kubectl_apply_view-last-applied.md docs/user-guide/kubectl/kubectl_attach.md docs/user-guide/kubectl/kubectl_auth.md docs/user-guide/kubectl/kubectl_auth_can-i.md +docs/user-guide/kubectl/kubectl_auth_reconcile.md docs/user-guide/kubectl/kubectl_autoscale.md docs/user-guide/kubectl/kubectl_certificate.md docs/user-guide/kubectl/kubectl_certificate_approve.md diff --git a/docs/man/man1/kubectl-auth-reconcile.1 b/docs/man/man1/kubectl-auth-reconcile.1 new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/man/man1/kubectl-auth-reconcile.1 @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/docs/user-guide/kubectl/kubectl_auth_reconcile.md b/docs/user-guide/kubectl/kubectl_auth_reconcile.md new file mode 100644 index 0000000000..b6fd7a0f98 --- /dev/null +++ b/docs/user-guide/kubectl/kubectl_auth_reconcile.md @@ -0,0 +1,3 @@ +This file is autogenerated, but we've stopped checking such files into the +repository to reduce the need for rebases. Please run hack/generate-docs.sh to +populate this file. diff --git a/hack/make-rules/test-cmd-util.sh b/hack/make-rules/test-cmd-util.sh index a7ac65f198..34d1f7a5a8 100755 --- a/hack/make-rules/test-cmd-util.sh +++ b/hack/make-rules/test-cmd-util.sh @@ -4645,6 +4645,17 @@ runTests() { kube::test::if_empty_string "${output_message}" fi + # kubectl auth reconcile + if kube::test::if_supports_resource "${clusterroles}" ; then + kubectl auth reconcile "${kube_flags[@]}" -f test/fixtures/pkg/kubectl/cmd/auth/rbac-resource-plus.yaml + kube::test::get_object_assert 'rolebindings -n some-other-random -l test-cmd=auth' "{{range.items}}{{$id_field}}:{{end}}" 'testing-RB:' + kube::test::get_object_assert 'roles -n some-other-random -l test-cmd=auth' "{{range.items}}{{$id_field}}:{{end}}" 'testing-R:' + kube::test::get_object_assert 'clusterrolebindings -l test-cmd=auth' "{{range.items}}{{$id_field}}:{{end}}" 'testing-CRB:' + kube::test::get_object_assert 'clusterroles -l test-cmd=auth' "{{range.items}}{{$id_field}}:{{end}}" 'testing-CR:' + + kubectl delete "${kube_flags[@]}" rolebindings,role,clusterroles,clusterrolebindings -n some-other-random -l test-cmd=auth + fi + ##################### # Retrieve multiple # ##################### diff --git a/pkg/kubectl/cmd/auth/BUILD b/pkg/kubectl/cmd/auth/BUILD index a5b957d4b2..cc29869d07 100644 --- a/pkg/kubectl/cmd/auth/BUILD +++ b/pkg/kubectl/cmd/auth/BUILD @@ -9,15 +9,22 @@ go_library( srcs = [ "auth.go", "cani.go", + "reconcile.go", ], visibility = [ "//build/visible_to:pkg_kubectl_cmd_auth_CONSUMERS", ], deps = [ "//pkg/apis/authorization:go_default_library", + "//pkg/apis/rbac:go_default_library", "//pkg/client/clientset_generated/internalclientset/typed/authorization/internalversion:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/core/internalversion:go_default_library", + "//pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion:go_default_library", "//pkg/kubectl/cmd/templates:go_default_library", "//pkg/kubectl/cmd/util:go_default_library", + "//pkg/kubectl/resource:go_default_library", + "//pkg/registry/rbac/reconciliation:go_default_library", + "//vendor/github.com/golang/glog:go_default_library", "//vendor/github.com/spf13/cobra:go_default_library", "//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library", "//vendor/k8s.io/apimachinery/pkg/runtime/schema:go_default_library", diff --git a/pkg/kubectl/cmd/auth/auth.go b/pkg/kubectl/cmd/auth/auth.go index 689767a1d6..eb70d3005d 100644 --- a/pkg/kubectl/cmd/auth/auth.go +++ b/pkg/kubectl/cmd/auth/auth.go @@ -34,6 +34,7 @@ func NewCmdAuth(f cmdutil.Factory, out, errOut io.Writer) *cobra.Command { } cmds.AddCommand(NewCmdCanI(f, out, errOut)) + cmds.AddCommand(NewCmdReconcile(f, out, errOut)) return cmds } diff --git a/pkg/kubectl/cmd/auth/reconcile.go b/pkg/kubectl/cmd/auth/reconcile.go new file mode 100644 index 0000000000..5ce505eec6 --- /dev/null +++ b/pkg/kubectl/cmd/auth/reconcile.go @@ -0,0 +1,223 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package auth + +import ( + "errors" + "io" + + "github.com/golang/glog" + "github.com/spf13/cobra" + + "k8s.io/kubernetes/pkg/apis/rbac" + internalcoreclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" + internalrbacclient "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" + "k8s.io/kubernetes/pkg/kubectl/cmd/templates" + cmdutil "k8s.io/kubernetes/pkg/kubectl/cmd/util" + "k8s.io/kubernetes/pkg/kubectl/resource" + "k8s.io/kubernetes/pkg/registry/rbac/reconciliation" +) + +// ReconcileOptions is the start of the data required to perform the operation. As new fields are added, add them here instead of +// referencing the cmd.Flags() +type ReconcileOptions struct { + ResourceBuilder *resource.Builder + RBACClient internalrbacclient.RbacInterface + CoreClient internalcoreclient.CoreInterface + + Print func(*resource.Info) error + + Out io.Writer + Err io.Writer +} + +var ( + reconcileLong = templates.LongDesc(` + Reconciles rules for RBAC Role, RoleBinding, ClusterRole, and ClusterRole binding objects. + + This is preferred to 'apply' for RBAC resources so that proper rule coverage checks are done.`) + + reconcileExample = templates.Examples(` + # Reconcile rbac resources from a file + kubectl auth reconcile -f my-rbac-rules.yaml`) +) + +func NewCmdReconcile(f cmdutil.Factory, out, err io.Writer) *cobra.Command { + fileOptions := &resource.FilenameOptions{} + o := &ReconcileOptions{ + Out: out, + Err: err, + } + + cmd := &cobra.Command{ + Use: "reconcile -f FILENAME", + Short: "Reconciles rules for RBAC Role, RoleBinding, ClusterRole, and ClusterRole binding objects", + Long: reconcileLong, + Example: reconcileExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(cmd, f, args, fileOptions)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.RunReconcile()) + }, + } + + cmdutil.AddPrinterFlags(cmd) + usage := "identifying the resource to reconcile." + cmdutil.AddFilenameOptionFlags(cmd, fileOptions, usage) + cmd.MarkFlagRequired("filename") + + return cmd +} + +func (o *ReconcileOptions) Complete(cmd *cobra.Command, f cmdutil.Factory, args []string, options *resource.FilenameOptions) error { + if len(args) > 0 { + return errors.New("no arguments are allowed") + } + + namespace, enforceNamespace, err := f.DefaultNamespace() + if err != nil { + return err + } + o.ResourceBuilder = f.NewBuilder(true). + ContinueOnError(). + NamespaceParam(namespace).DefaultNamespace(). + FilenameParam(enforceNamespace, options). + Flatten() + + client, err := f.ClientSet() + if err != nil { + return err + } + o.RBACClient = client.Rbac() + o.CoreClient = client.Core() + + mapper, _ := f.Object() + dryRun := false + output := cmdutil.GetFlagString(cmd, "output") + shortOutput := output == "name" + o.Print = func(info *resource.Info) error { + if len(output) > 0 && !shortOutput { + return cmdutil.PrintResourceInfoForCommand(cmd, info, f, o.Out) + } + cmdutil.PrintSuccess(mapper, shortOutput, o.Out, info.Mapping.Resource, info.Name, dryRun, "reconciled") + return nil + } + + return nil +} + +func (o *ReconcileOptions) Validate() error { + return nil +} + +func (o *ReconcileOptions) RunReconcile() error { + r := o.ResourceBuilder.Do() + err := r.Err() + if err != nil { + return err + } + + err = r.Visit(func(info *resource.Info, err error) error { + if err != nil { + return err + } + + // shallowInfoCopy this is used to later twiddle the Object for printing + // we really need more straightforward printing options + shallowInfoCopy := *info + + switch t := info.Object.(type) { + case *rbac.Role: + reconcileOptions := reconciliation.ReconcileRoleOptions{ + Confirm: true, + RemoveExtraPermissions: false, + Role: reconciliation.RoleRuleOwner{Role: t}, + Client: reconciliation.RoleModifier{ + NamespaceClient: o.CoreClient.Namespaces(), + Client: o.RBACClient, + }, + } + result, err := reconcileOptions.Run() + if err != nil { + return err + } + shallowInfoCopy.Object = result.Role.GetObject() + o.Print(&shallowInfoCopy) + return nil + + case *rbac.ClusterRole: + reconcileOptions := reconciliation.ReconcileRoleOptions{ + Confirm: true, + RemoveExtraPermissions: false, + Role: reconciliation.ClusterRoleRuleOwner{ClusterRole: t}, + Client: reconciliation.ClusterRoleModifier{ + Client: o.RBACClient.ClusterRoles(), + }, + } + result, err := reconcileOptions.Run() + if err != nil { + return err + } + shallowInfoCopy.Object = result.Role.GetObject() + o.Print(&shallowInfoCopy) + return nil + + case *rbac.RoleBinding: + reconcileOptions := reconciliation.ReconcileRoleBindingOptions{ + Confirm: true, + RemoveExtraSubjects: false, + RoleBinding: reconciliation.RoleBindingAdapter{RoleBinding: t}, + Client: reconciliation.RoleBindingClientAdapter{ + Client: o.RBACClient, + NamespaceClient: o.CoreClient.Namespaces(), + }, + } + result, err := reconcileOptions.Run() + if err != nil { + return err + } + shallowInfoCopy.Object = result.RoleBinding.GetObject() + o.Print(&shallowInfoCopy) + return nil + + case *rbac.ClusterRoleBinding: + reconcileOptions := reconciliation.ReconcileRoleBindingOptions{ + Confirm: true, + RemoveExtraSubjects: false, + RoleBinding: reconciliation.ClusterRoleBindingAdapter{ClusterRoleBinding: t}, + Client: reconciliation.ClusterRoleBindingClientAdapter{ + Client: o.RBACClient.ClusterRoleBindings(), + }, + } + result, err := reconcileOptions.Run() + if err != nil { + return err + } + shallowInfoCopy.Object = result.RoleBinding.GetObject() + o.Print(&shallowInfoCopy) + return nil + + default: + glog.V(1).Infof("skipping %#v", info.Object.GetObjectKind()) + // skip ignored resources + } + + return nil + }) + + return err +} diff --git a/pkg/registry/rbac/reconciliation/BUILD b/pkg/registry/rbac/reconciliation/BUILD index c4755f054e..6f5d4f7c6b 100644 --- a/pkg/registry/rbac/reconciliation/BUILD +++ b/pkg/registry/rbac/reconciliation/BUILD @@ -40,6 +40,7 @@ go_library( "//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library", "//vendor/k8s.io/apimachinery/pkg/apis/meta/v1:go_default_library", "//vendor/k8s.io/apimachinery/pkg/conversion:go_default_library", + "//vendor/k8s.io/apimachinery/pkg/runtime:go_default_library", "//vendor/k8s.io/apimachinery/pkg/types:go_default_library", ], ) diff --git a/pkg/registry/rbac/reconciliation/clusterrole_interfaces.go b/pkg/registry/rbac/reconciliation/clusterrole_interfaces.go index f5c597ed96..cd4f221f37 100644 --- a/pkg/registry/rbac/reconciliation/clusterrole_interfaces.go +++ b/pkg/registry/rbac/reconciliation/clusterrole_interfaces.go @@ -18,6 +18,7 @@ package reconciliation import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/apis/rbac" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" ) @@ -29,6 +30,10 @@ type ClusterRoleRuleOwner struct { ClusterRole *rbac.ClusterRole } +func (o ClusterRoleRuleOwner) GetObject() runtime.Object { + return o.ClusterRole +} + func (o ClusterRoleRuleOwner) GetNamespace() string { return o.ClusterRole.Namespace } diff --git a/pkg/registry/rbac/reconciliation/clusterrolebinding_interfaces.go b/pkg/registry/rbac/reconciliation/clusterrolebinding_interfaces.go index 673aea76cd..aa07f107f8 100644 --- a/pkg/registry/rbac/reconciliation/clusterrolebinding_interfaces.go +++ b/pkg/registry/rbac/reconciliation/clusterrolebinding_interfaces.go @@ -18,6 +18,7 @@ package reconciliation import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/apis/rbac" "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/rbac/internalversion" @@ -30,6 +31,10 @@ type ClusterRoleBindingAdapter struct { ClusterRoleBinding *rbac.ClusterRoleBinding } +func (o ClusterRoleBindingAdapter) GetObject() runtime.Object { + return o.ClusterRoleBinding +} + func (o ClusterRoleBindingAdapter) GetNamespace() string { return o.ClusterRoleBinding.Namespace } diff --git a/pkg/registry/rbac/reconciliation/reconcile_role.go b/pkg/registry/rbac/reconciliation/reconcile_role.go index 92203dc332..873b329b0a 100644 --- a/pkg/registry/rbac/reconciliation/reconcile_role.go +++ b/pkg/registry/rbac/reconciliation/reconcile_role.go @@ -21,6 +21,7 @@ import ( "reflect" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/apis/rbac" "k8s.io/kubernetes/pkg/registry/rbac/validation" ) @@ -41,6 +42,7 @@ type RuleOwnerModifier interface { } type RuleOwner interface { + GetObject() runtime.Object GetNamespace() string GetName() string GetLabels() map[string]string diff --git a/pkg/registry/rbac/reconciliation/reconcile_rolebindings.go b/pkg/registry/rbac/reconciliation/reconcile_rolebindings.go index 5c2e20dff2..ac138da584 100644 --- a/pkg/registry/rbac/reconciliation/reconcile_rolebindings.go +++ b/pkg/registry/rbac/reconciliation/reconcile_rolebindings.go @@ -21,6 +21,7 @@ import ( "reflect" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/apis/rbac" ) @@ -33,6 +34,7 @@ type RoleBindingModifier interface { } type RoleBinding interface { + GetObject() runtime.Object GetNamespace() string GetName() string GetUID() types.UID diff --git a/pkg/registry/rbac/reconciliation/role_interfaces.go b/pkg/registry/rbac/reconciliation/role_interfaces.go index c01de504e2..f671c8c43b 100644 --- a/pkg/registry/rbac/reconciliation/role_interfaces.go +++ b/pkg/registry/rbac/reconciliation/role_interfaces.go @@ -19,6 +19,7 @@ package reconciliation import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/rbac" core "k8s.io/kubernetes/pkg/client/clientset_generated/internalclientset/typed/core/internalversion" @@ -32,6 +33,10 @@ type RoleRuleOwner struct { Role *rbac.Role } +func (o RoleRuleOwner) GetObject() runtime.Object { + return o.Role +} + func (o RoleRuleOwner) GetNamespace() string { return o.Role.Namespace } diff --git a/pkg/registry/rbac/reconciliation/rolebinding_interfaces.go b/pkg/registry/rbac/reconciliation/rolebinding_interfaces.go index baccbe75a0..2f26fc9541 100644 --- a/pkg/registry/rbac/reconciliation/rolebinding_interfaces.go +++ b/pkg/registry/rbac/reconciliation/rolebinding_interfaces.go @@ -19,6 +19,7 @@ package reconciliation import ( apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/apis/rbac" @@ -33,6 +34,10 @@ type RoleBindingAdapter struct { RoleBinding *rbac.RoleBinding } +func (o RoleBindingAdapter) GetObject() runtime.Object { + return o.RoleBinding +} + func (o RoleBindingAdapter) GetNamespace() string { return o.RoleBinding.Namespace } diff --git a/test/fixtures/pkg/kubectl/cmd/auth/rbac-resource-plus.yaml b/test/fixtures/pkg/kubectl/cmd/auth/rbac-resource-plus.yaml new file mode 100644 index 0000000000..c358d8aa28 --- /dev/null +++ b/test/fixtures/pkg/kubectl/cmd/auth/rbac-resource-plus.yaml @@ -0,0 +1,88 @@ +apiVersion: v1 +items: +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRole + metadata: + labels: + test-cmd: auth + name: testing-CR + rules: + - apiGroups: + - "" + resources: + - pods + verbs: + - create + - delete + - deletecollection + - get + - list + - patch + - update + - watch + +- apiVersion: v1 + kind: Pod + metadata: + name: valid-pod + labels: + name: valid-pod + spec: + containers: + - name: kubernetes-serve-hostname + image: gcr.io/google_containers/serve_hostname + resources: + limits: + cpu: "1" + memory: 512Mi + +- apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + labels: + test-cmd: auth + name: testing-CRB + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: testing-CR + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:masters + +- apiVersion: rbac.authorization.k8s.io/v1 + kind: RoleBinding + metadata: + labels: + test-cmd: auth + name: testing-RB + namespace: some-other-random + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: testing-CR + subjects: + - apiGroup: rbac.authorization.k8s.io + kind: Group + name: system:masters + +- apiVersion: rbac.authorization.k8s.io/v1 + kind: Role + metadata: + labels: + test-cmd: auth + name: testing-R + namespace: some-other-random + rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - get + - list + - watch + +kind: List +metadata: {}