mirror of https://github.com/k3s-io/k3s
442 lines
12 KiB
Go
442 lines
12 KiB
Go
/*
|
|
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 create
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
rbacv1 "k8s.io/api/rbac/v1"
|
|
"k8s.io/apimachinery/pkg/api/meta"
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
"k8s.io/apimachinery/pkg/util/sets"
|
|
"k8s.io/cli-runtime/pkg/genericclioptions"
|
|
"k8s.io/cli-runtime/pkg/resource"
|
|
clientgorbacv1 "k8s.io/client-go/kubernetes/typed/rbac/v1"
|
|
cmdutil "k8s.io/kubectl/pkg/cmd/util"
|
|
"k8s.io/kubectl/pkg/scheme"
|
|
"k8s.io/kubectl/pkg/util"
|
|
"k8s.io/kubectl/pkg/util/i18n"
|
|
"k8s.io/kubectl/pkg/util/templates"
|
|
)
|
|
|
|
var (
|
|
roleLong = templates.LongDesc(i18n.T(`
|
|
Create a role with single rule.`))
|
|
|
|
roleExample = templates.Examples(i18n.T(`
|
|
# Create a Role named "pod-reader" that allows user to perform "get", "watch" and "list" on pods
|
|
kubectl create role pod-reader --verb=get --verb=list --verb=watch --resource=pods
|
|
|
|
# Create a Role named "pod-reader" with ResourceName specified
|
|
kubectl create role pod-reader --verb=get --resource=pods --resource-name=readablepod --resource-name=anotherpod
|
|
|
|
# Create a Role named "foo" with API Group specified
|
|
kubectl create role foo --verb=get,list,watch --resource=rs.extensions
|
|
|
|
# Create a Role named "foo" with SubResource specified
|
|
kubectl create role foo --verb=get,list,watch --resource=pods,pods/status`))
|
|
|
|
// Valid resource verb list for validation.
|
|
validResourceVerbs = []string{"*", "get", "delete", "list", "create", "update", "patch", "watch", "proxy", "deletecollection", "use", "bind", "escalate", "impersonate"}
|
|
|
|
// Specialized verbs and GroupResources
|
|
specialVerbs = map[string][]schema.GroupResource{
|
|
"use": {
|
|
{
|
|
Group: "policy",
|
|
Resource: "podsecuritypolicies",
|
|
},
|
|
{
|
|
Group: "extensions",
|
|
Resource: "podsecuritypolicies",
|
|
},
|
|
},
|
|
"bind": {
|
|
{
|
|
Group: "rbac.authorization.k8s.io",
|
|
Resource: "roles",
|
|
},
|
|
{
|
|
Group: "rbac.authorization.k8s.io",
|
|
Resource: "clusterroles",
|
|
},
|
|
},
|
|
"escalate": {
|
|
{
|
|
Group: "rbac.authorization.k8s.io",
|
|
Resource: "roles",
|
|
},
|
|
{
|
|
Group: "rbac.authorization.k8s.io",
|
|
Resource: "clusterroles",
|
|
},
|
|
},
|
|
"impersonate": {
|
|
{
|
|
Group: "",
|
|
Resource: "users",
|
|
},
|
|
{
|
|
Group: "",
|
|
Resource: "serviceaccounts",
|
|
},
|
|
{
|
|
Group: "",
|
|
Resource: "groups",
|
|
},
|
|
{
|
|
Group: "authentication.k8s.io",
|
|
Resource: "userextras",
|
|
},
|
|
},
|
|
}
|
|
)
|
|
|
|
// ResourceOptions holds the related options for '--resource' option
|
|
type ResourceOptions struct {
|
|
Group string
|
|
Resource string
|
|
SubResource string
|
|
}
|
|
|
|
// CreateRoleOptions holds the options for 'create role' sub command
|
|
type CreateRoleOptions struct {
|
|
PrintFlags *genericclioptions.PrintFlags
|
|
|
|
Name string
|
|
Verbs []string
|
|
Resources []ResourceOptions
|
|
ResourceNames []string
|
|
|
|
DryRunStrategy cmdutil.DryRunStrategy
|
|
DryRunVerifier *resource.DryRunVerifier
|
|
OutputFormat string
|
|
Namespace string
|
|
EnforceNamespace bool
|
|
Client clientgorbacv1.RbacV1Interface
|
|
Mapper meta.RESTMapper
|
|
PrintObj func(obj runtime.Object) error
|
|
FieldManager string
|
|
CreateAnnotation bool
|
|
|
|
genericclioptions.IOStreams
|
|
}
|
|
|
|
// NewCreateRoleOptions returns an initialized CreateRoleOptions instance
|
|
func NewCreateRoleOptions(ioStreams genericclioptions.IOStreams) *CreateRoleOptions {
|
|
return &CreateRoleOptions{
|
|
PrintFlags: genericclioptions.NewPrintFlags("created").WithTypeSetter(scheme.Scheme),
|
|
|
|
IOStreams: ioStreams,
|
|
}
|
|
}
|
|
|
|
// NewCmdCreateRole returnns an initialized Command instance for 'create role' sub command
|
|
func NewCmdCreateRole(f cmdutil.Factory, ioStreams genericclioptions.IOStreams) *cobra.Command {
|
|
o := NewCreateRoleOptions(ioStreams)
|
|
|
|
cmd := &cobra.Command{
|
|
Use: "role NAME --verb=verb --resource=resource.group/subresource [--resource-name=resourcename] [--dry-run=server|client|none]",
|
|
DisableFlagsInUseLine: true,
|
|
Short: roleLong,
|
|
Long: roleLong,
|
|
Example: roleExample,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cmdutil.CheckErr(o.Complete(f, cmd, args))
|
|
cmdutil.CheckErr(o.Validate())
|
|
cmdutil.CheckErr(o.RunCreateRole())
|
|
},
|
|
}
|
|
|
|
o.PrintFlags.AddFlags(cmd)
|
|
|
|
cmdutil.AddApplyAnnotationFlags(cmd)
|
|
cmdutil.AddValidateFlags(cmd)
|
|
cmdutil.AddDryRunFlag(cmd)
|
|
cmd.Flags().StringSliceVar(&o.Verbs, "verb", o.Verbs, "Verb that applies to the resources contained in the rule")
|
|
cmd.Flags().StringSlice("resource", []string{}, "Resource that the rule applies to")
|
|
cmd.Flags().StringArrayVar(&o.ResourceNames, "resource-name", o.ResourceNames, "Resource in the white list that the rule applies to, repeat this flag for multiple items")
|
|
cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-create")
|
|
return cmd
|
|
}
|
|
|
|
// Complete completes all the required options
|
|
func (o *CreateRoleOptions) Complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error {
|
|
name, err := NameFromCommandArgs(cmd, args)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Name = name
|
|
|
|
// Remove duplicate verbs.
|
|
verbs := []string{}
|
|
for _, v := range o.Verbs {
|
|
// VerbAll respresents all kinds of verbs.
|
|
if v == "*" {
|
|
verbs = []string{"*"}
|
|
break
|
|
}
|
|
if !arrayContains(verbs, v) {
|
|
verbs = append(verbs, v)
|
|
}
|
|
}
|
|
o.Verbs = verbs
|
|
|
|
// Support resource.group pattern. If no API Group specified, use "" as core API Group.
|
|
// e.g. --resource=pods,deployments.extensions
|
|
resources := cmdutil.GetFlagStringSlice(cmd, "resource")
|
|
for _, r := range resources {
|
|
sections := strings.SplitN(r, "/", 2)
|
|
|
|
resource := &ResourceOptions{}
|
|
if len(sections) == 2 {
|
|
resource.SubResource = sections[1]
|
|
}
|
|
|
|
parts := strings.SplitN(sections[0], ".", 2)
|
|
if len(parts) == 2 {
|
|
resource.Group = parts[1]
|
|
}
|
|
resource.Resource = parts[0]
|
|
|
|
if resource.Resource == "*" && len(parts) == 1 && len(sections) == 1 {
|
|
o.Resources = []ResourceOptions{*resource}
|
|
break
|
|
}
|
|
|
|
o.Resources = append(o.Resources, *resource)
|
|
}
|
|
|
|
// Remove duplicate resource names.
|
|
resourceNames := []string{}
|
|
for _, n := range o.ResourceNames {
|
|
if !arrayContains(resourceNames, n) {
|
|
resourceNames = append(resourceNames, n)
|
|
}
|
|
}
|
|
o.ResourceNames = resourceNames
|
|
|
|
// Complete other options for Run.
|
|
o.Mapper, err = f.ToRESTMapper()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
o.DryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dynamicClient, err := f.DynamicClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
discoveryClient, err := f.ToDiscoveryClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.DryRunVerifier = resource.NewDryRunVerifier(dynamicClient, discoveryClient)
|
|
o.OutputFormat = cmdutil.GetFlagString(cmd, "output")
|
|
o.CreateAnnotation = cmdutil.GetFlagBool(cmd, cmdutil.ApplyAnnotationsFlag)
|
|
|
|
cmdutil.PrintFlagsWithDryRunStrategy(o.PrintFlags, o.DryRunStrategy)
|
|
printer, err := o.PrintFlags.ToPrinter()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.PrintObj = func(obj runtime.Object) error {
|
|
return printer.PrintObj(obj, o.Out)
|
|
}
|
|
|
|
o.Namespace, o.EnforceNamespace, err = f.ToRawKubeConfigLoader().Namespace()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
clientset, err := f.KubernetesClientSet()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.Client = clientset.RbacV1()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate makes sure there is no discrepency in provided option values
|
|
func (o *CreateRoleOptions) Validate() error {
|
|
if o.Name == "" {
|
|
return fmt.Errorf("name must be specified")
|
|
}
|
|
|
|
// validate verbs.
|
|
if len(o.Verbs) == 0 {
|
|
return fmt.Errorf("at least one verb must be specified")
|
|
}
|
|
|
|
for _, v := range o.Verbs {
|
|
if !arrayContains(validResourceVerbs, v) {
|
|
fmt.Fprintf(o.ErrOut, "Warning: '%s' is not a standard resource verb\n", v)
|
|
}
|
|
}
|
|
|
|
// validate resources.
|
|
if len(o.Resources) == 0 {
|
|
return fmt.Errorf("at least one resource must be specified")
|
|
}
|
|
|
|
return o.validateResource()
|
|
}
|
|
|
|
func (o *CreateRoleOptions) validateResource() error {
|
|
for _, r := range o.Resources {
|
|
if len(r.Resource) == 0 {
|
|
return fmt.Errorf("resource must be specified if apiGroup/subresource specified")
|
|
}
|
|
if r.Resource == "*" {
|
|
return nil
|
|
}
|
|
|
|
resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
|
|
groupVersionResource, err := o.Mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
|
|
if err == nil {
|
|
resource = groupVersionResource
|
|
}
|
|
|
|
for _, v := range o.Verbs {
|
|
if groupResources, ok := specialVerbs[v]; ok {
|
|
match := false
|
|
for _, extra := range groupResources {
|
|
if resource.Resource == extra.Resource && resource.Group == extra.Group {
|
|
match = true
|
|
err = nil
|
|
break
|
|
}
|
|
}
|
|
if !match {
|
|
return fmt.Errorf("can not perform '%s' on '%s' in group '%s'", v, resource.Resource, resource.Group)
|
|
}
|
|
}
|
|
}
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// RunCreateRole performs the execution of 'create role' sub command
|
|
func (o *CreateRoleOptions) RunCreateRole() error {
|
|
role := &rbacv1.Role{
|
|
// this is ok because we know exactly how we want to be serialized
|
|
TypeMeta: metav1.TypeMeta{APIVersion: rbacv1.SchemeGroupVersion.String(), Kind: "Role"},
|
|
}
|
|
role.Name = o.Name
|
|
rules, err := generateResourcePolicyRules(o.Mapper, o.Verbs, o.Resources, o.ResourceNames, []string{})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
role.Rules = rules
|
|
if o.EnforceNamespace {
|
|
role.Namespace = o.Namespace
|
|
}
|
|
|
|
if err := util.CreateOrUpdateAnnotation(o.CreateAnnotation, role, scheme.DefaultJSONEncoder()); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create role.
|
|
if o.DryRunStrategy != cmdutil.DryRunClient {
|
|
createOptions := metav1.CreateOptions{}
|
|
if o.FieldManager != "" {
|
|
createOptions.FieldManager = o.FieldManager
|
|
}
|
|
if o.DryRunStrategy == cmdutil.DryRunServer {
|
|
if err := o.DryRunVerifier.HasSupport(role.GroupVersionKind()); err != nil {
|
|
return err
|
|
}
|
|
createOptions.DryRun = []string{metav1.DryRunAll}
|
|
}
|
|
role, err = o.Client.Roles(o.Namespace).Create(context.TODO(), role, createOptions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return o.PrintObj(role)
|
|
}
|
|
|
|
func arrayContains(s []string, e string) bool {
|
|
for _, a := range s {
|
|
if a == e {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func generateResourcePolicyRules(mapper meta.RESTMapper, verbs []string, resources []ResourceOptions, resourceNames []string, nonResourceURLs []string) ([]rbacv1.PolicyRule, error) {
|
|
// groupResourceMapping is a apigroup-resource map. The key of this map is api group, while the value
|
|
// is a string array of resources under this api group.
|
|
// E.g. groupResourceMapping = {"extensions": ["replicasets", "deployments"], "batch":["jobs"]}
|
|
groupResourceMapping := map[string][]string{}
|
|
|
|
// This loop does the following work:
|
|
// 1. Constructs groupResourceMapping based on input resources.
|
|
// 2. Prevents pointing to non-existent resources.
|
|
// 3. Transfers resource short name to long name. E.g. rs.extensions is transferred to replicasets.extensions
|
|
for _, r := range resources {
|
|
resource := schema.GroupVersionResource{Resource: r.Resource, Group: r.Group}
|
|
groupVersionResource, err := mapper.ResourceFor(schema.GroupVersionResource{Resource: r.Resource, Group: r.Group})
|
|
if err == nil {
|
|
resource = groupVersionResource
|
|
}
|
|
|
|
if len(r.SubResource) > 0 {
|
|
resource.Resource = resource.Resource + "/" + r.SubResource
|
|
}
|
|
if !arrayContains(groupResourceMapping[resource.Group], resource.Resource) {
|
|
groupResourceMapping[resource.Group] = append(groupResourceMapping[resource.Group], resource.Resource)
|
|
}
|
|
}
|
|
|
|
// Create separate rule for each of the api group.
|
|
rules := []rbacv1.PolicyRule{}
|
|
for _, g := range sets.StringKeySet(groupResourceMapping).List() {
|
|
rule := rbacv1.PolicyRule{}
|
|
rule.Verbs = verbs
|
|
rule.Resources = groupResourceMapping[g]
|
|
rule.APIGroups = []string{g}
|
|
rule.ResourceNames = resourceNames
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
if len(nonResourceURLs) > 0 {
|
|
rule := rbacv1.PolicyRule{}
|
|
rule.Verbs = verbs
|
|
rule.NonResourceURLs = nonResourceURLs
|
|
rules = append(rules, rule)
|
|
}
|
|
|
|
return rules, nil
|
|
}
|