diff --git a/cmd/aws-iam-authenticator/add.go b/cmd/aws-iam-authenticator/add.go new file mode 100644 index 000000000..595a0dae5 --- /dev/null +++ b/cmd/aws-iam-authenticator/add.go @@ -0,0 +1,196 @@ +/* +Copyright 2021 by the contributors. + +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 main + +import ( + "fmt" + "os" + + "github.com/manifoldco/promptui" + "github.com/spf13/cobra" + "k8s.io/client-go/kubernetes" + restclient "k8s.io/client-go/rest" + "k8s.io/client-go/tools/clientcmd" + clientcmd_api "k8s.io/client-go/tools/clientcmd/api" + "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/configmap/client" + "sigs.k8s.io/yaml" +) + +var addCmd = &cobra.Command{ + Use: "add", + Short: "add IAM entity to an existing aws-auth configmap", +} + +var addUserCmd = &cobra.Command{ + Use: "user", + Short: "add a user entity to an existing aws-auth configmap, not for CRD/file backends", + Long: "NOTE: this does not currently support the CRD and file backends", + Run: func(cmd *cobra.Command, args []string) { + if userARN == "" || userName == "" || len(groups) == 0 { + fmt.Printf("invalid empty value in userARN %q, username %q, groups %q", userARN, userName, groups) + os.Exit(1) + } + + checkPrompt(fmt.Sprintf("add userarn %s, username %s, groups %s", userARN, userName, groups)) + cli := createClient() + + cm, err := cli.AddUser(&config.UserMapping{ + UserARN: userARN, + Username: userName, + Groups: groups, + }) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + b, err := yaml.Marshal(cm) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("updated configmap:\n\n%s\n", string(b)) + }, +} + +var addRoleCmd = &cobra.Command{ + Use: "role", + Short: "add a role entity to an existing aws-auth configmap, not for CRD/file backends", + Long: "NOTE: this does not currently support the CRD and file backends", + Run: func(cmd *cobra.Command, args []string) { + if roleARN == "" || userName == "" || len(groups) == 0 { + fmt.Printf("invalid empty value in rolearn %q, username %q, groups %q", roleARN, userName, groups) + os.Exit(1) + } + + checkPrompt(fmt.Sprintf("add rolearn %s, username %s, groups %s", roleARN, userName, groups)) + cli := createClient() + + cm, err := cli.AddRole(&config.RoleMapping{ + RoleARN: roleARN, + Username: userName, + Groups: groups, + }) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + b, err := yaml.Marshal(cm) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + fmt.Printf("updated configmap:\n\n%s\n", string(b)) + }, +} + +func checkPrompt(action string) { + if !prompt { + return + } + + msg := fmt.Sprintf("Ready to add %q, should we continue?", action) + prompt := promptui.Select{ + Label: msg, + Items: []string{ + "No, cancel it!", + fmt.Sprintf("Yes, let's add %q!", action), + }, + } + idx, answer, err := prompt.Run() + if err != nil { + panic(err) + } + if idx != 1 { + fmt.Printf("cancelled %q [index %d, answer %q]\n", action, idx, answer) + os.Exit(0) + } +} + +func createClient() client.Client { + if kubeconfigPath == "" { + fmt.Println("empty kubeconfig") + os.Exit(1) + } + + var kcfg *restclient.Config + var err error + if kubeconfigContext != "" { + kcfg, err = clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ + ExplicitPath: kubeconfigPath, + }, + &clientcmd.ConfigOverrides{ + CurrentContext: kubeconfigContext, + ClusterInfo: clientcmd_api.Cluster{Server: masterURL}, + }, + ).ClientConfig() + } else { + kcfg, err = clientcmd.BuildConfigFromFlags(masterURL, kubeconfigPath) + } + if err != nil { + fmt.Println(err) + } + if kcfg == nil { + defaultConfig := clientcmd.DefaultClientConfig + kcfg, err = defaultConfig.ClientConfig() + if kcfg == nil || err != nil { + fmt.Printf("failed to create config from defaults %v\n", err) + os.Exit(1) + } + } + clientset, err := kubernetes.NewForConfig(kcfg) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + return client.New(clientset.CoreV1().ConfigMaps("kube-system")) +} + +var ( + prompt bool + masterURL string + kubeconfigPath string + kubeconfigContext string + + userARN string + userName string + groups []string + roleARN string +) + +func init() { + rootCmd.AddCommand(addCmd) + addCmd.AddCommand(addUserCmd) + addCmd.AddCommand(addRoleCmd) + + addCmd.PersistentFlags().BoolVar(&prompt, "prompt", true, "'false' to disable prompt'") + addCmd.PersistentFlags().StringVar(&masterURL, "master-url", "", "kube-apiserver URL for creating Kubernetes client") + addCmd.PersistentFlags().StringVar(&kubeconfigPath, "kubeconfig", "", "kubeconfig file path, if empty, it loads the default config") + addCmd.PersistentFlags().StringVar(&kubeconfigContext, "kubeconfig-context", "", "kubeconfig context, if empty, it uses the default context") + + addUserCmd.PersistentFlags().StringVar(&userARN, "userarn", "", "A new user ARN") + addUserCmd.PersistentFlags().StringVar(&userName, "username", "", "A new user name") + addUserCmd.PersistentFlags().StringSliceVar(&groups, "groups", nil, "A new user groups") + + addRoleCmd.PersistentFlags().StringVar(&roleARN, "rolearn", "", "A new role ARN") + addRoleCmd.PersistentFlags().StringVar(&userName, "username", "", "A new user name") + addRoleCmd.PersistentFlags().StringSliceVar(&groups, "groups", nil, "A new role groups") +} diff --git a/go.mod b/go.mod index 0449746a1..275d9a32f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/aws/aws-sdk-go v1.37.1 github.com/gofrs/flock v0.7.0 + github.com/manifoldco/promptui v0.8.0 github.com/prometheus/client_golang v1.1.0 github.com/sirupsen/logrus v1.4.2 github.com/spf13/cobra v0.0.5 @@ -18,4 +19,5 @@ require ( k8s.io/code-generator v0.16.8 k8s.io/component-base v0.16.8 k8s.io/sample-controller v0.16.8 + sigs.k8s.io/yaml v1.1.0 ) diff --git a/go.sum b/go.sum index cee26ffbf..716703f93 100644 --- a/go.sum +++ b/go.sum @@ -30,6 +30,12 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver v3.5.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= +github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= +github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= +github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= @@ -77,7 +83,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/gofrs/flock v0.7.0 h1:pGFUjl501gafK9HBt1VGL1KCOd/YhIooID+xgyJCf3g= github.com/gofrs/flock v0.7.0/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d h1:3PaI8p3seN09VjbTYC/QWlUZdZ1qS1zGjy7LH2Wt07I= github.com/gogo/protobuf v1.2.2-0.20190723190241-65acae22fc9d/go.mod h1:SlYgWuQ5SjCEi6WLHjHCa1yvBfUnHcTbrrZtXPKa29o= @@ -112,7 +117,6 @@ github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:Fecb github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/hashicorp/golang-lru v0.5.0 h1:CL2msUPvZTLb5O648aiLNJw3hnBxN2+1Jq8rCOH9wdo= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -134,6 +138,8 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV github.com/json-iterator/go v1.1.7 h1:KfgG9LzI+pYjr4xvmz/5H4FXjokeP+rlHLhv3iH62Fo= github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU= +github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= @@ -147,12 +153,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw= +github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= +github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= @@ -170,7 +184,6 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.6.0 h1:Ix8l273rp3QzYgXSR+c8d1fTG7UPgYkOSELPhiY/YGw= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -215,7 +228,6 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= @@ -226,7 +238,6 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= @@ -257,9 +268,7 @@ golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190611184440-5c40567a22f8/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975 h1:/Tl7pH94bvbAAHBdZJT947M/+gp0+CqQXDtMRC0fseo= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -288,9 +297,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80 h1:Ao/3l156eZf2AW5wK8a7/smtodRU+gha3+BeqJ69lRk= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191004110552-13f9640d40b9 h1:rjwSpXsdiK0dV8/Naq3kAw9ymfAeJIyd0upUIElB+lI= golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= @@ -309,6 +316,7 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -317,16 +325,13 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190616124812-15dcb6c0061f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff h1:u5LtynOOWSPG+jkEa3Y4ATlQ05vVeRvFjYSvbG0z6uw= golang.org/x/sys v0.0.0-20190812172437-4e8604ab3aff/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a h1:aYOabOQFp6Vj6W1F80affTUvO9UxmJRx8K0gsfABByQ= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -344,7 +349,6 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190614205625-5aca471b1d59/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190812233024-afc3694995b6 h1:TDcU4GQ226bJxMRWvn7/cVmdet+ms7oAElM2dabOio0= golang.org/x/tools v0.0.0-20190812233024-afc3694995b6/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -377,7 +381,6 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/config/types.go b/pkg/config/types.go index 8f6225dfd..13bdc6c63 100644 --- a/pkg/config/types.go +++ b/pkg/config/types.go @@ -44,28 +44,28 @@ type IdentityMapping struct { // You can use plain values without parameters to have a more static mapping. type RoleMapping struct { // RoleARN is the AWS Resource Name of the role. (e.g., "arn:aws:iam::000000000000:role/Foo"). - RoleARN string + RoleARN string `json:"rolearn"` // Username is the username pattern that this instances assuming this // role will have in Kubernetes. - Username string + Username string `json:"username"` // Groups is a list of Kubernetes groups this role will authenticate // as (e.g., `system:masters`). Each group name can include placeholders. - Groups []string + Groups []string `json:"groups"` } // UserMapping is a static mapping of a single AWS User ARN to a // Kubernetes username and a list of Kubernetes groups type UserMapping struct { // UserARN is the AWS Resource Name of the user. (e.g., "arn:aws:iam::000000000000:user/Test"). - UserARN string + UserARN string `json:"userarn"` // Username is the Kubernetes username this role will authenticate as (e.g., `mycorp:foo`) - Username string + Username string `json:"username"` // Groups is a list of Kubernetes groups this role will authenticate as (e.g., `system:masters`) - Groups []string + Groups []string `json:"groups"` } // Config specifies the configuration for a aws-iam-authenticator server diff --git a/pkg/mapper/configmap/client/client.go b/pkg/mapper/configmap/client/client.go new file mode 100644 index 000000000..152bea37a --- /dev/null +++ b/pkg/mapper/configmap/client/client.go @@ -0,0 +1,113 @@ +// Package client implements client-side operations on auth configmap. +package client + +import ( + "errors" + "fmt" + + "github.com/sirupsen/logrus" + core_v1 "k8s.io/api/core/v1" + k8s_errors "k8s.io/apimachinery/pkg/api/errors" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + client_v1 "k8s.io/client-go/kubernetes/typed/core/v1" + "k8s.io/client-go/util/retry" + "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/configmap" +) + +// Client defines configmap client methods. +type Client interface { + AddRole(role *config.RoleMapping) (*core_v1.ConfigMap, error) + AddUser(user *config.UserMapping) (*core_v1.ConfigMap, error) +} + +const mapName = "aws-auth" + +// New creates a new "Client". +func New(cli client_v1.ConfigMapInterface) Client { + return &client{ + getMap: func() (*core_v1.ConfigMap, error) { + return cli.Get(mapName, meta_v1.GetOptions{}) + }, + updateMap: func(m *core_v1.ConfigMap) (cm *core_v1.ConfigMap, err error) { + cm, err = cli.Update(m) + return cm, err + }, + } +} + +type client struct { + // define as function types for testing + getMap func() (*core_v1.ConfigMap, error) + updateMap func(m *core_v1.ConfigMap) (cm *core_v1.ConfigMap, err error) +} + +func (cli *client) AddRole(role *config.RoleMapping) (*core_v1.ConfigMap, error) { + if role == nil { + return nil, errors.New("empty role") + } + return cli.add(role, nil) +} + +func (cli *client) AddUser(user *config.UserMapping) (*core_v1.ConfigMap, error) { + if user == nil { + return nil, errors.New("empty user") + } + return cli.add(nil, user) +} + +func (cli *client) add(role *config.RoleMapping, user *config.UserMapping) (cm *core_v1.ConfigMap, err error) { + if role == nil && user == nil { + return nil, errors.New("empty role/user") + } + err = retry.RetryOnConflict(retry.DefaultRetry, func() error { + cm, err = cli.getMap() + if err != nil { + if k8s_errors.IsNotFound(err) { + logrus.WithError(err).Warn("not found map " + mapName) + } + return err + } + + data := cm.Data + + userMappings, roleMappings, awsAccounts, err := configmap.ParseMap(data) + if err != nil { + return fmt.Errorf("failed to parse configmap %v", err) + } + + if role != nil { + for _, r := range roleMappings { + if r.RoleARN == role.RoleARN { + return fmt.Errorf("cannot add duplicate role ARN %q", role.RoleARN) + } + } + roleMappings = append(roleMappings, *role) + } + + if user != nil { + for _, r := range userMappings { + if r.UserARN == user.UserARN { + return fmt.Errorf("cannot add duplicate user ARN %q", user.UserARN) + } + } + userMappings = append(userMappings, *user) + } + + data, err = configmap.EncodeMap(userMappings, roleMappings, awsAccounts) + if err != nil { + return err + } + + cm.Data = data + + updatedCm, err := cli.updateMap(cm) + if err != nil { + return err + } + + cm = updatedCm + return nil + }) + return cm, err +} diff --git a/pkg/mapper/configmap/client/client_test.go b/pkg/mapper/configmap/client/client_test.go new file mode 100644 index 000000000..09e35cba3 --- /dev/null +++ b/pkg/mapper/configmap/client/client_test.go @@ -0,0 +1,85 @@ +package client + +import ( + "reflect" + "strings" + "testing" + + core_v1 "k8s.io/api/core/v1" + "sigs.k8s.io/aws-iam-authenticator/pkg/config" + "sigs.k8s.io/aws-iam-authenticator/pkg/mapper/configmap" +) + +func TestAddUser(t *testing.T) { + cli := makeTestClient(t, + nil, + []config.RoleMapping{ + {RoleARN: "a", Username: "a", Groups: []string{"a"}}, + }, + nil, + ) + newUser := config.UserMapping{UserARN: "a", Username: "a", Groups: []string{"a"}} + cm, err := cli.AddUser(&newUser) + if err != nil { + t.Fatal(err) + } + u, _, _, err := configmap.ParseMap(cm.Data) + if err != nil { + t.Fatal(err) + } + updatedUser := u[0] + if !reflect.DeepEqual(newUser, updatedUser) { + t.Fatalf("unexpected updated user %+v", updatedUser) + } + + if _, err := cli.AddRole(&config.RoleMapping{RoleARN: "a"}); err == nil || !strings.Contains(err.Error(), `cannot add duplicate role ARN`) { + t.Fatal(err) + } +} + +func TestAddRole(t *testing.T) { + cli := makeTestClient(t, + []config.UserMapping{ + {UserARN: "a", Username: "a", Groups: []string{"a"}}, + }, + nil, + nil, + ) + newRole := config.RoleMapping{RoleARN: "a", Username: "a", Groups: []string{"a"}} + cm, err := cli.AddRole(&newRole) + if err != nil { + t.Fatal(err) + } + _, r, _, err := configmap.ParseMap(cm.Data) + if err != nil { + t.Fatal(err) + } + updatedRole := r[0] + if !reflect.DeepEqual(newRole, updatedRole) { + t.Fatalf("unexpected updated role %+v", updatedRole) + } + + if _, err := cli.AddUser(&config.UserMapping{UserARN: "a"}); err == nil || !strings.Contains(err.Error(), `cannot add duplicate user ARN`) { + t.Fatal(err) + } +} + +func makeTestClient( + t *testing.T, + userMappings []config.UserMapping, + roleMappings []config.RoleMapping, + awsAccounts []string, +) Client { + d, err := configmap.EncodeMap(userMappings, roleMappings, awsAccounts) + if err != nil { + t.Fatal(err) + } + return &client{ + getMap: func() (*core_v1.ConfigMap, error) { + return &core_v1.ConfigMap{Data: d}, nil + }, + updateMap: func(m *core_v1.ConfigMap) (*core_v1.ConfigMap, error) { + return m, nil + }, + } +} diff --git a/pkg/mapper/configmap/configmap.go b/pkg/mapper/configmap/configmap.go index d97f9ee60..ec4496568 100644 --- a/pkg/mapper/configmap/configmap.go +++ b/pkg/mapper/configmap/configmap.go @@ -16,7 +16,7 @@ import ( utilyaml "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/apimachinery/pkg/watch" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/kubernetes/typed/core/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/aws-iam-authenticator/pkg/config" ) @@ -80,7 +80,7 @@ func (ms *MapStore) startLoadConfigMap(stopCh <-chan struct{}) { break } logrus.Info("Received aws-auth watch event") - userMappings, roleMappings, awsAccounts, err := ms.parseMap(cm.Data) + userMappings, roleMappings, awsAccounts, err := ParseMap(cm.Data) if err != nil { logrus.Errorf("There was an error parsing the config maps. Only saving data that was good, %+v", err) } @@ -106,10 +106,9 @@ func (err ErrParsingMap) Error() string { return fmt.Sprintf("error parsing config map: %v", err.errors) } -// Acquire lock before calling -func (ms *MapStore) parseMap(m map[string]string) ([]config.UserMapping, []config.RoleMapping, []string, error) { +func ParseMap(m map[string]string) (userMappings []config.UserMapping, roleMappings []config.RoleMapping, awsAccounts []string, err error) { errs := make([]error, 0) - userMappings := make([]config.UserMapping, 0) + userMappings = make([]config.UserMapping, 0) if userData, ok := m["mapUsers"]; ok { userJson, err := utilyaml.ToJSON([]byte(userData)) if err != nil { @@ -122,7 +121,7 @@ func (ms *MapStore) parseMap(m map[string]string) ([]config.UserMapping, []confi } } - roleMappings := make([]config.RoleMapping, 0) + roleMappings = make([]config.RoleMapping, 0) if roleData, ok := m["mapRoles"]; ok { roleJson, err := utilyaml.ToJSON([]byte(roleData)) if err != nil { @@ -135,7 +134,7 @@ func (ms *MapStore) parseMap(m map[string]string) ([]config.UserMapping, []confi } } - awsAccounts := make([]string, 0) + awsAccounts = make([]string, 0) if accountsData, ok := m["mapAccounts"]; ok { err := yaml.Unmarshal([]byte(accountsData), &awsAccounts) if err != nil { @@ -143,7 +142,6 @@ func (ms *MapStore) parseMap(m map[string]string) ([]config.UserMapping, []confi } } - var err error if len(errs) > 0 { logrus.Warnf("Errors parsing configmap: %+v", errs) err = ErrParsingMap{errors: errs} @@ -151,6 +149,36 @@ func (ms *MapStore) parseMap(m map[string]string) ([]config.UserMapping, []confi return userMappings, roleMappings, awsAccounts, err } +func EncodeMap(userMappings []config.UserMapping, roleMappings []config.RoleMapping, awsAccounts []string) (m map[string]string, err error) { + m = make(map[string]string) + + if len(userMappings) > 0 { + body, err := yaml.Marshal(userMappings) + if err != nil { + return nil, err + } + m["mapUsers"] = string(body) + } + + if len(roleMappings) > 0 { + body, err := yaml.Marshal(roleMappings) + if err != nil { + return nil, err + } + m["mapRoles"] = string(body) + } + + if len(awsAccounts) > 0 { + body, err := yaml.Marshal(awsAccounts) + if err != nil { + return nil, err + } + m["mapAccounts"] = string(body) + } + + return m, nil +} + func (ms *MapStore) saveMap(userMappings []config.UserMapping, roleMappings []config.RoleMapping, awsAccounts []string) { ms.mutex.Lock() defer ms.mutex.Unlock() diff --git a/pkg/mapper/configmap/configmap_test.go b/pkg/mapper/configmap/configmap_test.go index 9d656a4c0..ef80459f6 100644 --- a/pkg/mapper/configmap/configmap_test.go +++ b/pkg/mapper/configmap/configmap_test.go @@ -9,7 +9,7 @@ import ( core_v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/watch" - "k8s.io/client-go/kubernetes/typed/core/v1" + v1 "k8s.io/client-go/kubernetes/typed/core/v1" "k8s.io/client-go/kubernetes/typed/core/v1/fake" k8stesting "k8s.io/client-go/testing" "sigs.k8s.io/aws-iam-authenticator/pkg/config" @@ -234,3 +234,54 @@ func TestLoadConfigMap(t *testing.T) { } } + +func TestParseMap(t *testing.T) { + m1 := map[string]string{ + "mapRoles": `- rolearn: arn:aws:iam::123456789101:role/test-NodeInstanceRole-1VWRHZ3GKZ1T4 + username: system:node:{{EC2PrivateDNSName}} + groups: + - system:bootstrappers + - system:nodes +`, + "mapUsers": `- userarn: arn:aws:iam::123456789101:user/Hello + username: Hello + groups: + - system:masters +- userarn: arn:aws:iam::123456789101:user/World + username: World + groups: + - system:masters +`, + } + userMappings := []config.UserMapping{ + {UserARN: "arn:aws:iam::123456789101:user/Hello", Username: "Hello", Groups: []string{"system:masters"}}, + {UserARN: "arn:aws:iam::123456789101:user/World", Username: "World", Groups: []string{"system:masters"}}, + } + roleMappings := []config.RoleMapping{ + {RoleARN: "arn:aws:iam::123456789101:role/test-NodeInstanceRole-1VWRHZ3GKZ1T4", Username: "system:node:{{EC2PrivateDNSName}}", Groups: []string{"system:bootstrappers", "system:nodes"}}, + } + accounts := []string{} + + u, r, a, err := ParseMap(m1) + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(u, userMappings) { + t.Fatalf("unexpected userMappings %+v", u) + } + if !reflect.DeepEqual(r, roleMappings) { + t.Fatalf("unexpected roleMappings %+v", r) + } + if !reflect.DeepEqual(a, accounts) { + t.Fatalf("unexpected accounts %+v", a) + } + + m2, err := EncodeMap(u, r, a) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(m1, m2) { + t.Fatalf("unexpected %v != %v", m1, m2) + } +}