diff --git a/.gitignore b/.gitignore index 223ee9a87..63e5f12bc 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ deploy/deployment.yaml build /certs/ SAMToolkit.* +coverage.out diff --git a/Dockerfile b/Dockerfile index 749158e30..ce4ac1a85 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ FROM golang AS builder WORKDIR $GOPATH/src/github.com/aws/amazon-eks-pod-identity-webhook COPY . ./ -RUN CGO_ENABLED=0 GOOS=linux go build -v -a -installsuffix nocgo -o /webhook . +RUN GOPROXY=direct CGO_ENABLED=0 GOOS=linux go build -o /webhook -v -a -installsuffix nocgo -ldflags="-buildid='' -w -s" . FROM scratch COPY ATTRIBUTIONS.txt /ATTRIBUTIONS.txt diff --git a/Makefile b/Makefile index 1d3f658a8..b655a4e45 100644 --- a/Makefile +++ b/Makefile @@ -5,14 +5,13 @@ include ${BGO_MAKEFILE} export CGO_ENABLED=0 export T=github.com/aws/amazon-eks-pod-identity-webhook UNAME_S = $(shell uname -s) -GO_INSTALL_FLAGS = -ldflags="-s -w" +GO_LDFLAGS = -ldflags='-s -w -buildid=""' install:: build ifeq ($(UNAME_S), Darwin) - GOOS=darwin GOARCH=amd64 go build -o build/gopath/bin/darwin_amd64/amazon-eks-pod-identity-webhook $(GO_INSTALL_FLAGS) $V $T + GOOS=darwin GOARCH=amd64 go build -o build/gopath/bin/darwin_amd64/amazon-eks-pod-identity-webhook $(GO_LDFLAGS) $V $T endif - GOOS=linux GOARCH=amd64 go build -o build/gopath/bin/linux_amd64/amazon-eks-pod-identity-webhook $(GO_INSTALL_FLAGS) $V $T - + GOOS=linux GOARCH=amd64 go build -o build/gopath/bin/linux_amd64/amazon-eks-pod-identity-webhook $(GO_LDFLAGS) $V $T # Generic make REGISTRY_ID?=602401143452 @@ -20,6 +19,10 @@ IMAGE_NAME?=eks/pod-identity-webhook REGION?=us-west-2 IMAGE?=$(REGISTRY_ID).dkr.ecr.$(REGION).amazonaws.com/$(IMAGE_NAME) +test: + go test -coverprofile=coverage.out ./... + go tool cover -html=coverage.out + docker: @echo 'Building image $(IMAGE)...' docker build --no-cache -t $(IMAGE) . @@ -94,7 +97,7 @@ delete-config: clean:: rm -rf ./amazon-eks-pod-identity-webhook - rm -rf ./certs/ + rm -rf ./certs/ coverage.out .PHONY: docker push build local-serve local-request cluster-up cluster-down prep-config deploy-config delete-config clean diff --git a/README.md b/README.md index a9cbc735b..8c0ef9298 100644 --- a/README.md +++ b/README.md @@ -21,17 +21,17 @@ This webhook is for mutating pods that will require AWS IAM access. { "Effect": "Allow", "Principal": { - "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85" + "Federated": "arn:aws:iam::111122223333:oidc-provider/oidc.REGION.eks.amazonaws.com/CLUSTER_ID" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "__doc_comment": "scope the role to the service account (optional)", "StringEquals": { - "oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85:sub": "system:serviceaccount:default:my-serviceaccount" + "oidc.REGION.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:my-serviceaccount" }, "__doc_comment": "scope the role to a namespace (optional)", "StringLike": { - "oidc.us-west-2.eks.amazonaws.com/624a142e-43fc-4a4e-9a65-0adbfe9d6a85:sub": "system:serviceaccount:default:*" + "oidc.REGION.eks.amazonaws.com/CLUSTER_ID:sub": "system:serviceaccount:default:*" } } } @@ -48,6 +48,11 @@ This webhook is for mutating pods that will require AWS IAM access. namespace: default annotations: eks.amazonaws.com/role-arn: "arn:aws:iam::111122223333:role/s3-reader" + # optional: Defaults to "sts.amazonaws.com" if not set + eks.amazonaws.com/audience: "sts.amazonaws.com" + # optional: When set to "true", adds AWS_STS_REGIONAL_ENDPOINTS env var + # to containers + eks.amazonaws.com/sts-regional-endpoints: "true" ``` 4. All new pod pods launched using this Service Account will be modified to use IAM for pods. Below is an example pod spec with the environment variables and @@ -58,9 +63,18 @@ This webhook is for mutating pods that will require AWS IAM access. metadata: name: my-pod namespace: default + annotations: + # optional: A comma-separated list of initContainers and container names + # to skip adding volumes and environemnt variables + eks.amazonaws.com/skip-containers: "init-first,sidecar" spec: serviceAccountName: my-serviceaccount + initContainers: + - name: init-first + image: container-image:version containers: + - name: sidecar + image: container-image:version - name: container-name image: container-image:version ### Everything below is added by the webhook ### @@ -73,6 +87,8 @@ This webhook is for mutating pods that will require AWS IAM access. value: "arn:aws:iam::111122223333:role/s3-reader" - name: AWS_WEB_IDENTITY_TOKEN_FILE value: "/var/run/secrets/eks.amazonaws.com/serviceaccount/token" + - name: AWS_STS_REGIONAL_ENDPOINTS + value: "regional" volumeMounts: - mountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount/" name: aws-token @@ -85,7 +101,7 @@ This webhook is for mutating pods that will require AWS IAM access. expirationSeconds: 86400 path: token ``` - + ### Usage with Windows container workloads To ensure workloads are scheduled on windows nodes have the right environment variables, they must have a `nodeSelector` targeting windows it must run on. Workloads targeting windows nodes using `nodeAffinity` are currently not supported. @@ -125,12 +141,14 @@ Usage of amazon-eks-pod-identity-webhook: --log_file string If non-empty, use this log file --log_file_max_size uint Defines the maximum size a log file can grow to. Unit is megabytes. If the value is 0, the maximum file size is unlimited. (default 1800) --logtostderr log to standard error instead of files (default true) + --metrics-port int Port to listen on for metrics and healthz (http) (default 9999) --namespace string (in-cluster) The namespace name this webhook and the tls secret resides in (default "eks") --port int Port to listen on (default 443) --service-name string (in-cluster) The service name fronting this webhook (default "pod-identity-webhook") --skip_headers If true, avoid header prefixes in the log messages --skip_log_headers If true, avoid headers when openning log files --stderrthreshold severity logs at or above this threshold go to stderr (default 2) + --sts-regional-endpoint false Whether to inject the AWS_STS_REGIONAL_ENDPOINTS=regional env var in mutated pods. Defaults to false. --tls-cert string (out-of-cluster) TLS certificate file path (default "/etc/webhook/certs/tls.crt") --tls-key string (out-of-cluster) TLS key file path (default "/etc/webhook/certs/tls.key") --tls-secret string (in-cluster) The secret name for storing the TLS serving cert (default "pod-identity-webhook") @@ -146,6 +164,18 @@ Usage of amazon-eks-pod-identity-webhook: When the `aws-default-region` flag is set this webhook will inject `AWS_DEFAULT_REGION` and `AWS_REGION` in mutated containers if `AWS_DEFAULT_REGION` and `AWS_REGION` are not already set. +### AWS_STS_REGIONAL_ENDPOINTS Injection + +When the `sts-regional-endpoint` flag is set to `true`, the webhook will +inject the environment variable `AWS_STS_REGIONAL_ENDPOINTS` with the value set +to `regional`. This environment variable will configure the AWS SDKs to perform +the `sts:AssumeRoleWithWebIdentity` call to get credentials from the regional +endpoint, instead of the global endpoint in `us-east-1`. This is desirable in +almost all cases, unless the STS regional endpoint is [disabled in your +account](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_temp_enable-regions.html). + +You can also enable this per-service account with the annotation +`eks.amazonaws.com/sts-regional-endpoint` set to `"true"`. ## Container Images @@ -171,9 +201,6 @@ For self-hosted API server configuration, see see [SELF_HOSTED_SETUP.md](/SELF_H ### On API server TODO -## Development -TODO - ## Code of Conduct See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) diff --git a/go.mod b/go.mod index bd38379be..0b895a852 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/docker/distribution v2.7.1+incompatible // indirect github.com/evanphx/json-patch v4.4.0+incompatible // indirect github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef // indirect + github.com/google/go-cmp v0.5.2 // indirect github.com/google/gofuzz v1.0.0 // indirect github.com/googleapis/gnostic v0.2.0 // indirect github.com/hashicorp/golang-lru v0.5.1 // indirect @@ -13,11 +14,13 @@ require ( github.com/json-iterator/go v1.1.6 // indirect github.com/modern-go/reflect2 v1.0.1 // indirect github.com/opencontainers/go-digest v1.0.0-rc1 // indirect + github.com/pkg/errors v0.8.0 github.com/prometheus/client_golang v0.9.3 github.com/spf13/pflag v1.0.3 golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/square/go-jose.v2 v2.5.1 k8s.io/api v0.0.0-20190606204050-af9c91bd2759 k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d k8s.io/client-go v11.0.1-0.20190606204521-b8faab9c5193+incompatible @@ -25,5 +28,5 @@ require ( k8s.io/kube-openapi v0.0.0-20190603182131-db7b694dc208 // indirect k8s.io/kubernetes v1.14.3 k8s.io/utils v0.0.0-20190529001817-6999998975a7 // indirect - sigs.k8s.io/yaml v1.1.0 // indirect + sigs.k8s.io/yaml v1.1.0 ) diff --git a/go.sum b/go.sum index e71cf1062..e555b10f1 100644 --- a/go.sum +++ b/go.sum @@ -36,6 +36,8 @@ github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -122,6 +124,8 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZe golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09 h1:6Cq5LXQ/D2J5E7sYJemWSQApczOzY1rxSp8TWloyxIY= golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= @@ -129,6 +133,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/square/go-jose.v2 v2.5.1 h1:7odma5RETjNHWJnR32wx8t+Io4djHE1PqxCFx3iiZ2w= +gopkg.in/square/go-jose.v2 v2.5.1/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= k8s.io/api v0.0.0-20190606204050-af9c91bd2759 h1:T8xTLSBgKsq1bkiAwG9xamEydWVpBv9fHl5S/TDh3OU= diff --git a/main.go b/main.go index 8bae63538..07424a978 100644 --- a/main.go +++ b/main.go @@ -62,6 +62,7 @@ func main() { mountPath := flag.String("token-mount-path", "/var/run/secrets/eks.amazonaws.com/serviceaccount", "The path to mount tokens") tokenExpiration := flag.Int64("token-expiration", 86400, "The token expiration") region := flag.String("aws-default-region", "", "If set, AWS_DEFAULT_REGION and AWS_REGION will be set to this value in mutated containers") + regionalSTS := flag.Bool("sts-regional-endpoint", false, "Whether to inject the AWS_STS_REGIONAL_ENDPOINTS=regional env var in mutated pods. Defaults to `false`.") version := flag.Bool("version", false, "Display the version and exit") @@ -96,15 +97,18 @@ func main() { saCache := cache.New( *audience, *annotationPrefix, + *regionalSTS, clientset, ) saCache.Start() mod := handler.NewModifier( + handler.WithAnnotationDomain(*annotationPrefix), handler.WithExpiration(*tokenExpiration), handler.WithMountPath(*mountPath), handler.WithServiceAccountCache(saCache), handler.WithRegion(*region), + handler.WithRegionalSTS(*regionalSTS), ) addr := fmt.Sprintf(":%d", *port) @@ -124,7 +128,6 @@ func main() { fmt.Fprintf(w, "ok") }) - tlsConfig := &tls.Config{} if *inCluster { @@ -180,8 +183,8 @@ func main() { handler.ShutdownOnTerm(server, time.Duration(10)*time.Second) metricsServer := &http.Server{ - Addr: metricsAddr, - Handler: metricsMux, + Addr: metricsAddr, + Handler: metricsMux, } go func() { diff --git a/pkg/annotations.go b/pkg/annotations.go new file mode 100644 index 000000000..4b6625243 --- /dev/null +++ b/pkg/annotations.go @@ -0,0 +1,27 @@ +/* + Copyright 2010 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + A copy of the License is located at + + http://www.apache.org/licenses/LICENSE-2.0 + + or in the "license" file accompanying this file. This file 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 pkg + +const ( + // The audience annotation + AudienceAnnotation = "audience" + // Role ARN annotation + RoleARNAnnotation = "role-arn" + // A true/false value to add AWS_STS_REGIONAL_ENDPOINTS. Overrides any setting on the webhook + UseRegionalSTSAnnotation = "sts-regional-endpoints" + + // A comma-separated list of container names to skip adding environment variables and volumes to. Applies to `initContainers` and `containers` + SkipContainersAnnotation = "skip-containers" +) diff --git a/pkg/cache/cache.go b/pkg/cache/cache.go index 0eba51bdf..b899c6af4 100644 --- a/pkg/cache/cache.go +++ b/pkg/cache/cache.go @@ -17,10 +17,12 @@ package cache import ( "fmt" + "strconv" "sync" "time" - v1 "k8s.io/api/core/v1" + "github.com/aws/amazon-eks-pod-identity-webhook/pkg" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/fields" "k8s.io/apimachinery/pkg/labels" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -30,32 +32,34 @@ import ( ) type CacheResponse struct { - RoleARN string - Audience string + RoleARN string + Audience string + UseRegionalSTS bool } type ServiceAccountCache interface { Start() - Get(name, namespace string) (role, aud string) + Get(name, namespace string) (role, aud string, useRegionalSTS bool) } type serviceAccountCache struct { - mu sync.RWMutex // guards cache - cache map[string]*CacheResponse - store cache.Store - controller cache.Controller - clientset kubernetes.Interface - annotationPrefix string - defaultAudience string + mu sync.RWMutex // guards cache + cache map[string]*CacheResponse + store cache.Store + controller cache.Controller + clientset kubernetes.Interface + annotationPrefix string + defaultAudience string + defaultRegionalSTS bool } -func (c *serviceAccountCache) Get(name, namespace string) (role, aud string) { +func (c *serviceAccountCache) Get(name, namespace string) (role, aud string, useRegionalSTS bool) { klog.V(5).Infof("Fetching sa %s/%s from cache", namespace, name) resp := c.get(name, namespace) if resp == nil { - return "", "" + return "", "", false } - return resp.RoleARN, resp.Audience + return resp.RoleARN, resp.Audience, resp.UseRegionalSTS } func (c *serviceAccountCache) get(name, namespace string) *CacheResponse { @@ -76,14 +80,23 @@ func (c *serviceAccountCache) pop(name, namespace string) { } func (c *serviceAccountCache) addSA(sa *v1.ServiceAccount) { - arn, ok := sa.Annotations[c.annotationPrefix+"/role-arn"] + arn, ok := sa.Annotations[c.annotationPrefix+"/"+pkg.RoleARNAnnotation] resp := &CacheResponse{} if ok { resp.RoleARN = arn - if audience, ok := sa.Annotations[c.annotationPrefix+"/audience"]; ok { + resp.Audience = c.defaultAudience + if audience, ok := sa.Annotations[c.annotationPrefix+"/"+pkg.AudienceAnnotation]; ok { resp.Audience = audience - } else { - resp.Audience = c.defaultAudience + } + + resp.UseRegionalSTS = c.defaultRegionalSTS + if disableRegionalStr, ok := sa.Annotations[c.annotationPrefix+"/"+pkg.UseRegionalSTSAnnotation]; ok { + disableRegional, err := strconv.ParseBool(disableRegionalStr) + if err != nil { + klog.V(4).Infof("Ignoring service account %s/%s invalid value for disable-regional-sts annotation", sa.Namespace, sa.Name) + } else { + resp.UseRegionalSTS = !disableRegional + } } } klog.V(5).Infof("Adding sa %s/%s to cache", sa.Name, sa.Namespace) @@ -96,11 +109,12 @@ func (c *serviceAccountCache) set(name, namespace string, resp *CacheResponse) { c.cache[namespace+"/"+name] = resp } -func New(defaultAudience, prefix string, clientset kubernetes.Interface) ServiceAccountCache { +func New(defaultAudience, prefix string, defaultRegionalSTS bool, clientset kubernetes.Interface) ServiceAccountCache { c := &serviceAccountCache{ - cache: map[string]*CacheResponse{}, - defaultAudience: defaultAudience, - annotationPrefix: prefix, + cache: map[string]*CacheResponse{}, + defaultAudience: defaultAudience, + annotationPrefix: prefix, + defaultRegionalSTS: defaultRegionalSTS, } saListWatcher := cache.NewListWatchFromClient( diff --git a/pkg/cache/cache_test.go b/pkg/cache/cache_test.go index f9d26126c..c49d185ca 100644 --- a/pkg/cache/cache_test.go +++ b/pkg/cache/cache_test.go @@ -11,7 +11,10 @@ func TestSaCache(t *testing.T) { testSA.Name = "default" testSA.Namespace = "default" roleArn := "arn:aws:iam::111122223333:role/s3-reader" - testSA.Annotations = map[string]string{"eks.amazonaws.com/role-arn": roleArn} + testSA.Annotations = map[string]string{ + "eks.amazonaws.com/role-arn": roleArn, + "eks.amazonaws.com/sts-regional-endpoints": "true", + } cache := &serviceAccountCache{ cache: map[string]*CacheResponse{}, @@ -19,20 +22,22 @@ func TestSaCache(t *testing.T) { annotationPrefix: "eks.amazonaws.com", } - role, aud := cache.Get("default", "default") + role, aud, useRegionalSTS := cache.Get("default", "default") if role != "" || aud != "" { - t.Errorf("Expected role and aud to be empty, got %s, %s", role, aud) + t.Errorf("Expected role and aud to be empty, got %s, %s, %t", role, aud, useRegionalSTS) } cache.addSA(testSA) - role, aud = cache.Get("default", "default") + role, aud, useRegionalSTS = cache.Get("default", "default") if role != roleArn { t.Errorf("Expected role to be %s, got %s", roleArn, role) } if aud != "sts.amazonaws.com" { t.Errorf("Expected aud to be sts.amzonaws.com, got %s", aud) } - + if useRegionalSTS { + t.Error("Expected regional STS to be true, got false") + } } diff --git a/pkg/cache/fake.go b/pkg/cache/fake.go index d4a8c55a9..73d258430 100644 --- a/pkg/cache/fake.go +++ b/pkg/cache/fake.go @@ -2,6 +2,7 @@ package cache import ( "k8s.io/api/core/v1" + "strconv" "sync" ) @@ -21,8 +22,10 @@ func NewFakeServiceAccountCache(accounts ...*v1.ServiceAccount) *FakeServiceAcco if !ok { audience = "sts.amazonaws.com" } + regionalSTSstr, _ := sa.Annotations["eks.amazonaws.com/sts-regional-endpoints"] + regionalSTS, _ := strconv.ParseBool(regionalSTSstr) - c.Add(sa.Name, sa.Namespace, arn, audience) + c.Add(sa.Name, sa.Namespace, arn, audience, regionalSTS) } return c } @@ -33,23 +36,24 @@ var _ ServiceAccountCache = &FakeServiceAccountCache{} func (f *FakeServiceAccountCache) Start() {} // Get gets a service account from the cache -func (f *FakeServiceAccountCache) Get(name, namespace string) (role, aud string) { +func (f *FakeServiceAccountCache) Get(name, namespace string) (role, aud string, useRegionalSTS bool) { f.mu.RLock() defer f.mu.RUnlock() resp, ok := f.cache[namespace+"/"+name] if !ok { - return "", "" + return "", "", false } - return resp.RoleARN, resp.Audience + return resp.RoleARN, resp.Audience, resp.UseRegionalSTS } // Add adds a cache entry -func (f *FakeServiceAccountCache) Add(name, namespace, role, aud string) { +func (f *FakeServiceAccountCache) Add(name, namespace, role, aud string, regionalSTS bool) { f.mu.Lock() defer f.mu.Unlock() f.cache[namespace+"/"+name] = &CacheResponse{ - RoleARN: role, - Audience: aud, + RoleARN: role, + Audience: aud, + UseRegionalSTS: regionalSTS, } } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 43d1bc87e..12749c3a0 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -16,6 +16,7 @@ package handler import ( + "encoding/csv" "encoding/json" "fmt" "io/ioutil" @@ -23,6 +24,7 @@ import ( "path/filepath" "strings" + "github.com/aws/amazon-eks-pod-identity-webhook/pkg" "github.com/aws/amazon-eks-pod-identity-webhook/pkg/cache" "k8s.io/api/admission/v1beta1" admissionregistrationv1beta1 "k8s.io/api/admissionregistration/v1beta1" @@ -34,6 +36,34 @@ import ( "k8s.io/kubernetes/pkg/apis/core/v1" ) +type podUpdateSettings struct { + skipContainers map[string]bool + useRegionalSTS bool +} + +// newPodUpdateSettings returns the update settings for a particular pod +func newPodUpdateSettings(annotationDomain string, pod *corev1.Pod, useRegionalSTS bool) *podUpdateSettings { + settings := &podUpdateSettings{ + useRegionalSTS: useRegionalSTS, + } + + skippedNames := map[string]bool{} + skipContainersKey := annotationDomain + "/" + pkg.SkipContainersAnnotation + if value, ok := pod.Annotations[skipContainersKey]; ok { + r := csv.NewReader(strings.NewReader(value)) + // error means we don't skip any + podNames, err := r.Read() + if err != nil { + klog.Infof("Could parse skip containers annotation on pod %s/%s: %v", pod.Namespace, pod.Name, err) + } + for _, name := range podNames { + skippedNames[name] = true + } + } + settings.skipContainers = skippedNames + return settings +} + func init() { _ = corev1.AddToScheme(runtimeScheme) _ = admissionregistrationv1beta1.AddToScheme(runtimeScheme) @@ -69,14 +99,25 @@ func WithRegion(region string) ModifierOpt { return func(m *Modifier) { m.Region = region } } +// WithRegionalSTS sets the modifier RegionalSTSEndpoint +func WithRegionalSTS(enabled bool) ModifierOpt { + return func(m *Modifier) { m.RegionalSTSEndpoint = enabled } +} + +// WithAnnotationDomain adds an annotation domain +func WithAnnotationDomain(domain string) ModifierOpt { + return func(m *Modifier) { m.AnnotationDomain = domain } +} + // NewModifier returns a Modifier with default values func NewModifier(opts ...ModifierOpt) *Modifier { - mod := &Modifier{ - MountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount", - Expiration: 86400, - volName: "aws-iam-token", - tokenName: "token", + AnnotationDomain: "eks.amazonaws.com", + MountPath: "/var/run/secrets/eks.amazonaws.com/serviceaccount", + Expiration: 86400, + RegionalSTSEndpoint: false, + volName: "aws-iam-token", + tokenName: "token", } for _, opt := range opts { opt(mod) @@ -87,12 +128,14 @@ func NewModifier(opts ...ModifierOpt) *Modifier { // Modifier holds configuration values for pod modifications type Modifier struct { - Expiration int64 - MountPath string - Region string - Cache cache.ServiceAccountCache - volName string - tokenName string + AnnotationDomain string + Expiration int64 + MountPath string + Region string + RegionalSTSEndpoint bool + Cache cache.ServiceAccountCache + volName string + tokenName string } type patchOperation struct { @@ -101,49 +144,68 @@ type patchOperation struct { Value interface{} `json:"value,omitempty"` } -func addEnvToContainer(container *corev1.Container, mountPath, tokenFilePath, volName, roleName, region string) { - var skipReservedKeys, skipRegionKey bool +func (m *Modifier) addEnvToContainer(container *corev1.Container, tokenFilePath, roleName string, podSettings *podUpdateSettings) { + // return if this is a named skipped container + if _, ok := podSettings.skipContainers[container.Name]; ok { + return + } + + var ( + reservedKeysDefined bool + regionKeyDefined bool + regionalStsKeyDefined bool + ) reservedKeys := map[string]string{ "AWS_ROLE_ARN": "", "AWS_WEB_IDENTITY_TOKEN_FILE": "", } - for _, env := range container.Env { - if _, ok := reservedKeys[env.Name]; ok { - // Skip if any env vars are already present - skipReservedKeys = true - } - } - awsRegionKeys := map[string]string{ "AWS_REGION": "", "AWS_DEFAULT_REGION": "", } + stsKey := "AWS_STS_REGIONAL_ENDPOINTS" for _, env := range container.Env { + if _, ok := reservedKeys[env.Name]; ok { + reservedKeysDefined = true + } if _, ok := awsRegionKeys[env.Name]; ok { - // Don't set AWS_DEFAULT_REGION if any awsRegionKeys is already set - skipRegionKey = true + // Don't set both region keys if any region key is already set + regionKeyDefined = true + } + if env.Name == stsKey { + regionalStsKeyDefined = true } } - if skipReservedKeys && skipRegionKey { + if reservedKeysDefined && regionKeyDefined && regionalStsKeyDefined { return } env := container.Env - if !skipRegionKey && region != "" { + + if !regionalStsKeyDefined && m.RegionalSTSEndpoint && podSettings.useRegionalSTS { + env = append(env, + corev1.EnvVar{ + Name: stsKey, + Value: "regional", + }, + ) + } + + if !regionKeyDefined && m.Region != "" { env = append(env, corev1.EnvVar{ Name: "AWS_DEFAULT_REGION", - Value: region, + Value: m.Region, }, corev1.EnvVar{ Name: "AWS_REGION", - Value: region, + Value: m.Region, }, ) } - if !skipReservedKeys { + if !reservedKeysDefined { env = append(env, corev1.EnvVar{ Name: "AWS_ROLE_ARN", Value: roleName, @@ -159,7 +221,7 @@ func addEnvToContainer(container *corev1.Container, mountPath, tokenFilePath, vo volExists := false for _, vol := range container.VolumeMounts { - if vol.Name == volName { + if vol.Name == m.volName { volExists = true } } @@ -168,15 +230,17 @@ func addEnvToContainer(container *corev1.Container, mountPath, tokenFilePath, vo container.VolumeMounts = append( container.VolumeMounts, corev1.VolumeMount{ - Name: volName, + Name: m.volName, ReadOnly: true, - MountPath: mountPath, + MountPath: m.MountPath, }, ) } } -func (m *Modifier) updatePodSpec(pod *corev1.Pod, roleName, audience string) []patchOperation { +func (m *Modifier) updatePodSpec(pod *corev1.Pod, roleName, audience string, regionalSTS bool) []patchOperation { + updateSettings := newPodUpdateSettings(m.AnnotationDomain, pod, regionalSTS) + tokenFilePath := filepath.Join(m.MountPath, m.tokenName) betaNodeSelector, _ := pod.Spec.NodeSelector["beta.kubernetes.io/os"] @@ -191,13 +255,13 @@ func (m *Modifier) updatePodSpec(pod *corev1.Pod, roleName, audience string) []p var initContainers = []corev1.Container{} for i := range pod.Spec.InitContainers { container := pod.Spec.InitContainers[i] - addEnvToContainer(&container, m.MountPath, tokenFilePath, m.volName, roleName, m.Region) + m.addEnvToContainer(&container, tokenFilePath, roleName, updateSettings) initContainers = append(initContainers, container) } var containers = []corev1.Container{} for i := range pod.Spec.Containers { container := pod.Spec.Containers[i] - addEnvToContainer(&container, m.MountPath, tokenFilePath, m.volName, roleName, m.Region) + m.addEnvToContainer(&container, tokenFilePath, roleName, updateSettings) containers = append(containers, container) } @@ -292,7 +356,7 @@ func (m *Modifier) MutatePod(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResp pod.Namespace = req.Namespace - podRole, audience := m.Cache.Get(pod.Spec.ServiceAccountName, pod.Namespace) + podRole, audience, regionalSTS := m.Cache.Get(pod.Spec.ServiceAccountName, pod.Namespace) // determine whether to perform mutation if podRole == "" { @@ -301,8 +365,7 @@ func (m *Modifier) MutatePod(ar *v1beta1.AdmissionReview) *v1beta1.AdmissionResp } } - patch := m.updatePodSpec(&pod, podRole, audience) - patchBytes, err := json.Marshal(patch) + patchBytes, err := json.Marshal(m.updatePodSpec(&pod, podRole, audience, regionalSTS)) if err != nil { klog.Errorf("Error marshaling pod update: %v", err.Error()) return &v1beta1.AdmissionResponse{ @@ -330,17 +393,12 @@ func (m *Modifier) Handle(w http.ResponseWriter, r *http.Request) { body = data } } - if len(body) == 0 { - klog.Errorf("empty body") - http.Error(w, "empty body", http.StatusBadRequest) - return - } // verify the content type is accurate contentType := r.Header.Get("Content-Type") if contentType != "application/json" { - klog.Errorf("Content-Type=%s, expect application/json", contentType) - http.Error(w, "invalid Content-Type, expect `application/json`", http.StatusUnsupportedMediaType) + klog.Errorf("Content-Type=%s, expected application/json", contentType) + http.Error(w, "Invalid Content-Type, expected `application/json`", http.StatusUnsupportedMediaType) return } diff --git a/pkg/handler/handler_pod_test.go b/pkg/handler/handler_pod_test.go new file mode 100644 index 000000000..ac27a7c67 --- /dev/null +++ b/pkg/handler/handler_pod_test.go @@ -0,0 +1,158 @@ +/* + Copyright 2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + A copy of the License is located at + + http://www.apache.org/licenses/LICENSE-2.0 + + or in the "license" file accompanying this file. This file 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 handler + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strconv" + "strings" + "testing" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/yaml" +) + +var fixtureDir = "./testdata" + +var ( + // SkipAnnotation means "don't test this file" + skipAnnotation = "testing.eks.amazonaws.com/skip" + // Expected patch output + expectedPatchAnnotation = "testing.eks.amazonaws.com/expectedPatch" + + // Service Account annotation values + roleArnSAAnnotation = "testing.eks.amazonaws.com/serviceAccount/roleArn" + audienceAnnotation = "testing.eks.amazonaws.com/serviceAccount/audience" + saInjectSTSAnnotation = "testing.eks.amazonaws.com/serviceAccount/sts-regional-endpoints" + + // Handler values + handlerMountPathAnnotation = "testing.eks.amazonaws.com/handler/mountPath" + handlerExpirationAnnotation = "testing.eks.amazonaws.com/handler/expiration" + handlerRegionAnnotation = "testing.eks.amazonaws.com/handler/region" + handlerSTSAnnotation = "testing.eks.amazonaws.com/handler/injectSTS" +) + +func getModifierFromPod(pod corev1.Pod) (*Modifier, error) { + modifiers := []ModifierOpt{} + + if path, ok := pod.Annotations[handlerMountPathAnnotation]; ok { + modifiers = append(modifiers, WithMountPath(path)) + } + if expStr, ok := pod.Annotations[handlerExpirationAnnotation]; ok { + expInt, err := strconv.Atoi(expStr) + if err != nil { + return nil, err + } + modifiers = append(modifiers, WithExpiration(int64(expInt))) + } + if region, ok := pod.Annotations[handlerRegionAnnotation]; ok { + modifiers = append(modifiers, WithRegion(region)) + } + if stsAnnotation, ok := pod.Annotations[handlerSTSAnnotation]; ok { + value, err := strconv.ParseBool(stsAnnotation) + if err != nil { + return nil, err + } + modifiers = append(modifiers, WithRegionalSTS(value)) + } + return NewModifier(modifiers...), nil +} + +func TestHandlePod(t *testing.T) { + err := filepath.Walk(fixtureDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + t.Errorf("Error while walking test fixtures: %v", err) + return err + } + if info.IsDir() { + return nil + } + if strings.HasSuffix(info.Name(), ".yaml") || strings.HasSuffix(info.Name(), ".yml") { + pod, err := parseFile(filepath.Join("./", path)) + if err != nil { + t.Errorf("Error while parsing file %s: %v", info.Name(), err) + return err + } + if skipStr, ok := pod.Annotations[skipAnnotation]; ok { + skip, _ := strconv.ParseBool(skipStr) + if skip { + return nil + } + } + + t.Run(fmt.Sprintf("Pod %s in file %s", pod.Name, path), func(t *testing.T) { + modifier, err := getModifierFromPod(*pod) + if err != nil { + t.Errorf("Error creating modifier: %v", err) + } + var roleARN string + if role, ok := pod.Annotations[roleArnSAAnnotation]; ok { + roleARN = role + } + audience := "sts.amazonaws.com" + if aud, ok := pod.Annotations[audienceAnnotation]; ok { + audience = aud + } + + useRegionalSTS := modifier.RegionalSTSEndpoint + if useRegionalSTSstr, ok := pod.Annotations[saInjectSTSAnnotation]; ok { + useRegionalSTS, err = strconv.ParseBool(useRegionalSTSstr) + if err != nil { + t.Errorf("Error parsing annotation %s: %v", saInjectSTSAnnotation, err) + } + } + + patchBytes, err := json.Marshal(modifier.updatePodSpec(pod, roleARN, audience, useRegionalSTS)) + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + expectedPatchStr, ok := pod.Annotations[expectedPatchAnnotation] + if !ok && (len(patchBytes) == 0 || patchBytes == nil) { + return + } + + if bytes.Compare(patchBytes, []byte(expectedPatchStr)) != 0 { + t.Errorf("Expected patch didn't match: \nGot\n\t%v\nWanted:\n\t%v\n", + string(patchBytes), + expectedPatchStr, + ) + } + + }) + return nil + } + return nil + }) + if err != nil { + t.Errorf("Error while walking test fixtures: %v", err) + } +} + +// Read in the first pod in the file +func parseFile(filename string) (*corev1.Pod, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + pod := &corev1.Pod{} + err = yaml.Unmarshal(data, pod) + return pod, err +} diff --git a/pkg/handler/handler_test.go b/pkg/handler/handler_test.go index 1f9e364b1..b590f5db3 100644 --- a/pkg/handler/handler_test.go +++ b/pkg/handler/handler_test.go @@ -16,7 +16,12 @@ package handler import ( + "bytes" "encoding/json" + "io" + "io/ioutil" + "net/http" + "net/http/httptest" "reflect" "testing" @@ -24,319 +29,80 @@ import ( "k8s.io/api/admission/v1beta1" authenticationv1 "k8s.io/api/authentication/v1" "k8s.io/api/core/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" ) -var rawPodWithoutVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default" - } -} -`) - -var rawWindowsBetaPodWithoutVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "nodeSelector": { - "beta.kubernetes.io/arch": "amd64", - "beta.kubernetes.io/os": "windows" +func TestMutatePod(t *testing.T) { + testServiceAccount := &v1.ServiceAccount{} + testServiceAccount.Name = "default" + testServiceAccount.Namespace = "default" + testServiceAccount.Annotations = map[string]string{ + "eks.amazonaws.com/role-arn": "arn:aws:iam::111122223333:role/s3-reader", } - } -} -`) -var rawWindowsPodWithoutVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "nodeSelector": { - "kubernetes.io/arch": "amd64", - "kubernetes.io/os": "windows" + modifier := NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))) + cases := []struct { + caseName string + input *v1beta1.AdmissionReview + response *v1beta1.AdmissionResponse + }{ + { + "nilBody", + nil, + &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, + }, + { + "NoRequest", + &v1beta1.AdmissionReview{Request: nil}, + &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, + }, } - } -} -`) -var rawPodWithVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "volumes": [ - { - "name": "my-volume" - } - ] - } -} -`) - -var rawPodWithInitContainer = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "initContainers": [ - { - "image": "amazonlinux", - "name": "initcontainer" - } - ], - "serviceAccountName": "default" - } -} -`) - -var rawPodWithIAMTokenVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "volumes": [ - { - "name": "aws-iam-token" - } - ] - } -} -`) + for _, c := range cases { + t.Run(c.caseName, func(t *testing.T) { + response := modifier.MutatePod(c.input) -var rawPodWithIAMTokenVolumeAndVolumeMount = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos", - "env": [ - { - "name": "AWS_ROLE_ARN", - "value": "arn:aws:iam::111122223333:role/s3-reader" - }, - { - "name": "AWS_WEB_IDENTITY_TOKEN_FILE", - "value": "/var/run/secrets/eks.amazonaws.com/serviceaccount/token" - } - ], - "volumeMounts": [ - { - "mountPath": "/var/run/secrets/eks.amazonaws.com/serviceaccount", - "name": "aws-iam-token", - "readOnly": true + if !reflect.DeepEqual(response, c.response) { + got, _ := json.MarshalIndent(response, "", " ") + want, _ := json.MarshalIndent(c.response, "", " ") + t.Errorf("Unexpected response. Got \n%s\n wanted \n%s", string(got), string(want)) } - ] - } - ], - "serviceAccountName": "default", - "volumes": [ - { - "name": "aws-iam-token" - } - ] - } -} -`) - -var rawWindowsBetaPodWithVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "nodeSelector": { - "beta.kubernetes.io/arch": "amd64", - "beta.kubernetes.io/os": "windows" - }, - "volumes": [ - { - "name": "my-volume" - } - ] - } + }) + } } -`) -var rawWindowsPodWithVolume = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default", - "nodeSelector": { - "kubernetes.io/arch": "amd64", - "kubernetes.io/os": "windows" - }, - "volumes": [ - { - "name": "my-volume" - } - ] - } -} -`) +var jsonPatchType = v1beta1.PatchType("JSONPatch") -var rawPodWithoutRegion = []byte(` +var rawPodWithoutVolume = []byte(` { "apiVersion": "v1", "kind": "Pod", "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" + "name": "balajilovesoreos", + "uid": "be8695c4-4ad0-4038-8786-c508853aa255" }, "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos" - } - ], - "serviceAccountName": "default" + "containers": [ + { + "image": "amazonlinux", + "name": "balajilovesoreos" + } + ], + "serviceAccountName": "default" } } `) -var rawPodWithAWSRegion = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos", - "env": [ - {"name":"AWS_REGION","value":"paris"} - ] - } - ], - "serviceAccountName": "default" - } -} -`) +var validPatchIfNoVolumesPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var rawPodWithAWSDefaultRegion = []byte(` -{ - "apiVersion": "v1", - "kind": "Pod", - "metadata": { - "name": "balajilovesoreos", - "uid": "be8695c4-4ad0-4038-8786-c508853aa255" - }, - "spec": { - "containers": [ - { - "image": "amazonlinux", - "name": "balajilovesoreos", - "env": [ - {"name":"AWS_DEFAULT_REGION","value":"paris"} - ] - } - ], - "serviceAccountName": "default" - } +var validHandlerResponse = &v1beta1.AdmissionResponse{ + UID: "918ef1dc-928f-4525-99ef-988389f263c3", + Allowed: true, + Patch: validPatchIfNoVolumesPresent, + PatchType: &jsonPatchType, } -`) func getValidReview(pod []byte) *v1beta1.AdmissionReview { return &v1beta1.AdmissionReview{ @@ -350,7 +116,7 @@ func getValidReview(pod []byte) *v1beta1.AdmissionReview { Operation: "CREATE", UserInfo: authenticationv1.UserInfo{ Username: "kubernetes-admin", - UID: "heptio-authenticator-aws:111122223333:AROAR2TG44V5CLZCFPOQZ", + UID: "aws-iam-authenticator:111122223333:AROAR2TG44V5CLZCFPOQZ", Groups: []string{"system:authenticated", "system:masters"}, }, Object: runtime.RawExtension{ @@ -362,232 +128,102 @@ func getValidReview(pod []byte) *v1beta1.AdmissionReview { } } -var validPatchIfNoVolumesPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfVolumesPresent = []byte(`[{"op":"add","path":"/spec/volumes/0","value":{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) - -var validPatchIfIAMTokenVolumePresent = []byte(`[{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfIAMTokenVolumeAndVolumeMountPresent = []byte(`[{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"},{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) - -var validPatchIfWindowsNoVolumesPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"C:\\var\\run\\secrets\\eks.amazonaws.com\\serviceaccount\\token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfWindowsVolumesPresent = []byte(`[{"op":"add","path":"/spec/volumes/0","value":{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"C:\\var\\run\\secrets\\eks.amazonaws.com\\serviceaccount\\token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) - -var validPatchIfNoRegionPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfRegionPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_REGION","value":"paris"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfDefaultRegionPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"paris"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) -var validPatchIfInitContainerPresent = []byte(`[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]},{"op":"add","path":"/spec/initContainers","value":[{"name":"initcontainer","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]`) - -var jsonPatchType = v1beta1.PatchType("JSONPatch") - -var validResponseIfNoVolumesPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfNoVolumesPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfVolumesPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfVolumesPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfIAMTokenVolumePresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfIAMTokenVolumePresent, - PatchType: &jsonPatchType, -} - -var validResponseIfIAMTokenVolumeAndVolumeMountPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfIAMTokenVolumeAndVolumeMountPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfWindowsNoVolumesPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfWindowsNoVolumesPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfWindowsVolumesPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfWindowsVolumesPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfNoRegionPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfNoRegionPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfRegionPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfRegionPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfDefaultRegionPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfDefaultRegionPresent, - PatchType: &jsonPatchType, -} - -var validResponseIfInitContainerPresent = &v1beta1.AdmissionResponse{ - UID: "", - Allowed: true, - Patch: validPatchIfInitContainerPresent, - PatchType: &jsonPatchType, +func serializeAdmissionReview(t *testing.T, want *v1beta1.AdmissionReview) []byte { + wantedBytes, err := json.Marshal(want) + if err != nil { + t.Errorf("Failed to marshal desired response: %v", err) + return nil + } + return wantedBytes } -func TestSecretStore(t *testing.T) { - testServiceAccount := &v1.ServiceAccount{} +func TestModifierHandler(t *testing.T) { + testServiceAccount := &corev1.ServiceAccount{} testServiceAccount.Name = "default" testServiceAccount.Namespace = "default" testServiceAccount.Annotations = map[string]string{ "eks.amazonaws.com/role-arn": "arn:aws:iam::111122223333:role/s3-reader", } + modifier := NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))) + + ts := httptest.NewServer( + http.HandlerFunc(modifier.Handle), + ) + defer ts.Close() + cases := []struct { - caseName string - modifier *Modifier - input *v1beta1.AdmissionReview - response *v1beta1.AdmissionResponse + caseName string + input []byte + inputContentType string + want []byte }{ { "nilBody", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), nil, - &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, + "application/json", + serializeAdmissionReview(t, &v1beta1.AdmissionReview{ + Response: &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, + }), }, { "NoRequest", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - &v1beta1.AdmissionReview{Request: nil}, - &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, - }, - { - "ValidRequestSuccessWithoutVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawPodWithoutVolume), - validResponseIfNoVolumesPresent, - }, - { - "ValidRequestSuccessWindowsWithoutVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawWindowsBetaPodWithoutVolume), - validResponseIfWindowsNoVolumesPresent, + serializeAdmissionReview(t, &v1beta1.AdmissionReview{Request: nil}), + "application/json", + serializeAdmissionReview(t, &v1beta1.AdmissionReview{ + Response: &v1beta1.AdmissionResponse{Result: &metav1.Status{Message: "bad content"}}, + }), }, { - "ValidRequestSuccessWindowsBetaWithoutVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawWindowsBetaPodWithoutVolume), - validResponseIfWindowsNoVolumesPresent, + "BadContentType", + serializeAdmissionReview(t, &v1beta1.AdmissionReview{Request: nil}), + "application/xml", + []byte("Invalid Content-Type, expected `application/json`\n"), }, { - "ValidRequestSuccessWithVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawPodWithVolume), - validResponseIfVolumesPresent, + "InvalidJSON", + []byte(`{"request": {"object": "\"metadata\":{\"name\":\"fake\""}`), + "application/json", + []byte(`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input"}}}`), }, { - "ValidRequestSuccessWindowsWithVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawWindowsPodWithVolume), - validResponseIfWindowsVolumesPresent, + "InvalidPodBytes", + []byte(`{"request": {"object": "\"metadata\":{\"name\":\"fake\""}}`), + "application/json", + []byte(`{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"json: cannot unmarshal string into Go value of type v1.Pod"}}}`), }, { - "ValidRequestSuccessWindowsBetaWithVolumes", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount))), - getValidReview(rawWindowsBetaPodWithVolume), - validResponseIfWindowsVolumesPresent, + "ValidRequestSuccessWithoutVolumes", + serializeAdmissionReview(t, getValidReview(rawPodWithoutVolume)), + "application/json", + serializeAdmissionReview(t, &v1beta1.AdmissionReview{Response: validHandlerResponse}), }, } for _, c := range cases { t.Run(c.caseName, func(t *testing.T) { - response := c.modifier.MutatePod(c.input) - - if !reflect.DeepEqual(response, c.response) { - got, _ := json.MarshalIndent(response, "", " ") - want, _ := json.MarshalIndent(c.response, "", " ") - t.Errorf("Unexpected response. Got \n%s\n wanted \n%s", string(got), string(want)) + var buf io.Reader + if c.input != nil { + buf = bytes.NewBuffer(c.input) } - - }) - } -} - -func TestEnvUpdate(t *testing.T) { - testServiceAccount := &v1.ServiceAccount{} - testServiceAccount.Name = "default" - testServiceAccount.Namespace = "default" - testServiceAccount.Annotations = map[string]string{ - "eks.amazonaws.com/role-arn": "arn:aws:iam::111122223333:role/s3-reader", - } - - cases := []struct { - caseName string - modifier *Modifier - input *v1beta1.AdmissionReview - response *v1beta1.AdmissionResponse - }{ - { - "ValidRequestSuccessWithoutRegion", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithoutVolume), - validResponseIfNoRegionPresent, - }, - { - "ValidRequestSuccessWithRegion", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithAWSRegion), - validResponseIfRegionPresent, - }, - { - "ValidRequestSuccessWithDefaultRegion", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithAWSDefaultRegion), - validResponseIfDefaultRegionPresent, - }, - { - "ValidRequestSuccessWithIAMTokenVolumePresent", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithIAMTokenVolume), - validResponseIfIAMTokenVolumePresent, - }, - { - "ValidRequestSuccessWithIAMTokenVolumeAndVolumeMountPresent", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithIAMTokenVolumeAndVolumeMount), - validResponseIfIAMTokenVolumeAndVolumeMountPresent, - }, - { - "ValidRequestSuccessWithInitContainer", - NewModifier(WithServiceAccountCache(cache.NewFakeServiceAccountCache(testServiceAccount)), WithRegion("seattle")), - getValidReview(rawPodWithInitContainer), - validResponseIfInitContainerPresent, - }, - } - - for _, c := range cases { - t.Run(c.caseName, func(t *testing.T) { - response := c.modifier.MutatePod(c.input) - - if !reflect.DeepEqual(response, c.response) { - got, _ := json.MarshalIndent(response, "", " ") - want, _ := json.MarshalIndent(c.response, "", " ") - t.Errorf("Unexpected response. Got \n%s\n wanted \n%s", string(got), string(want)) + resp, err := http.Post(ts.URL, c.inputContentType, buf) + if err != nil { + t.Errorf("Failed to make request: %v", err) + return + } + responseBytes, err := ioutil.ReadAll(resp.Body) + resp.Body.Close() + if err != nil { + t.Errorf("Failed to read response: %v", err) + return } + if bytes.Compare(responseBytes, c.want) != 0 { + t.Errorf("Expected response didn't match: \nGot\n\t\"%v\"\nWanted:\n\t\"%v\"\n", + string(responseBytes), + string(c.want), + ) + } }) } } diff --git a/pkg/handler/testdata/betaWindowsPodWithoutVolumes.pod.yaml b/pkg/handler/testdata/betaWindowsPodWithoutVolumes.pod.yaml new file mode 100644 index 000000000..f1186ecac --- /dev/null +++ b/pkg/handler/testdata/betaWindowsPodWithoutVolumes.pod.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + uid: be8695c4-4ad0-4038-8786-c508853aa255 + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"C:\\var\\run\\secrets\\eks.amazonaws.com\\serviceaccount\\token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + nodeSelector: + beta.kubernetes.io/arch: amd64 + beta.kubernetes.io/os: windows + serviceAccountName: default + diff --git a/pkg/handler/testdata/initPodNeedsRegion.pod.yaml b/pkg/handler/testdata/initPodNeedsRegion.pod.yaml new file mode 100644 index 000000000..4ed1d595b --- /dev/null +++ b/pkg/handler/testdata/initPodNeedsRegion.pod.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/region: "seattle" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]},{"op":"add","path":"/spec/initContainers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + initContainers: + - image: amazonlinux + name: balajilovesoreos + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodHasAll.pod.yaml b/pkg/handler/testdata/rawPodHasAll.pod.yaml new file mode 100644 index 000000000..896ce8588 --- /dev/null +++ b/pkg/handler/testdata/rawPodHasAll.pod.yaml @@ -0,0 +1,23 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws-cn:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/injectSTS: "true" + testing.eks.amazonaws.com/handler/region: "cn-north-1" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_REGION","value":"cn-northwest-1"},{"name":"AWS_STS_REGIONAL_ENDPOINTS","value":"regional"},{"name":"AWS_ROLE_ARN","value":"arn:aws-cn:iam::111122223333:role/s3-reader"}],"resources":{}}]}]' +spec: + containers: + - env: + - name: AWS_REGION + value: cn-northwest-1 + - name: AWS_STS_REGIONAL_ENDPOINTS + value: regional + - name: AWS_ROLE_ARN + value: 'arn:aws-cn:iam::111122223333:role/s3-reader' + image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodHasSTS.pod.yaml b/pkg/handler/testdata/rawPodHasSTS.pod.yaml new file mode 100644 index 000000000..0b7182c0e --- /dev/null +++ b/pkg/handler/testdata/rawPodHasSTS.pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws-cn:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/injectSTS: "true" + testing.eks.amazonaws.com/handler/region: "cn-north-1" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_REGION","value":"cn-northwest-1"},{"name":"AWS_STS_REGIONAL_ENDPOINTS","value":"regional"},{"name":"AWS_ROLE_ARN","value":"arn:aws-cn:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - env: + - name: AWS_REGION + value: cn-northwest-1 + - name: AWS_STS_REGIONAL_ENDPOINTS + value: regional + image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodHasVolumeMount.pod.yaml b/pkg/handler/testdata/rawPodHasVolumeMount.pod.yaml new file mode 100644 index 000000000..00d3665dd --- /dev/null +++ b/pkg/handler/testdata/rawPodHasVolumeMount.pod.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","mountPath":""}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + volumeMounts: + - name: aws-iam-token + path: /path/to/token + serviceAccountName: default + volumes: + - name: aws-iam-token + hostPath: + path: /path/to/token diff --git a/pkg/handler/testdata/rawPodNeedsRegion.pod.yaml b/pkg/handler/testdata/rawPodNeedsRegion.pod.yaml new file mode 100644 index 000000000..3fcd4e255 --- /dev/null +++ b/pkg/handler/testdata/rawPodNeedsRegion.pod.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/region: "seattle" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodNeedsSTS.pod.yaml b/pkg/handler/testdata/rawPodNeedsSTS.pod.yaml new file mode 100644 index 000000000..c2aede04a --- /dev/null +++ b/pkg/handler/testdata/rawPodNeedsSTS.pod.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws-cn:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/injectSTS: "true" + testing.eks.amazonaws.com/handler/region: "cn-north-1" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_STS_REGIONAL_ENDPOINTS","value":"regional"},{"name":"AWS_DEFAULT_REGION","value":"cn-north-1"},{"name":"AWS_REGION","value":"cn-north-1"},{"name":"AWS_ROLE_ARN","value":"arn:aws-cn:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodNoSTSAnnotationOverride.pod.yaml b/pkg/handler/testdata/rawPodNoSTSAnnotationOverride.pod.yaml new file mode 100644 index 000000000..317c667a5 --- /dev/null +++ b/pkg/handler/testdata/rawPodNoSTSAnnotationOverride.pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/serviceAccount/sts-regional-endpoints: "false" # SA annotation should override flag + testing.eks.amazonaws.com/handler/injectSTS: "true" + testing.eks.amazonaws.com/handler/region: "us-east-1" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"us-east-1"},{"name":"AWS_REGION","value":"us-east-1"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodRegionPresent.pod.yaml b/pkg/handler/testdata/rawPodRegionPresent.pod.yaml new file mode 100644 index 000000000..43eda21ee --- /dev/null +++ b/pkg/handler/testdata/rawPodRegionPresent.pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_REGION","value":"paris"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - env: + - name: AWS_REGION + value: paris + image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodSkip.pod.yaml b/pkg/handler/testdata/rawPodSkip.pod.yaml new file mode 100644 index 000000000..e9dcb8f3f --- /dev/null +++ b/pkg/handler/testdata/rawPodSkip.pod.yaml @@ -0,0 +1,22 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/region: "us-west-2" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"sidecar","image":"amazonlinux","resources":{}},{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"already-defined"},{"name":"AWS_DEFAULT_REGION","value":"us-west-2"},{"name":"AWS_REGION","value":"us-west-2"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' + # Pod Annotation + eks.amazonaws.com/skip-containers: "sidecar" +spec: + containers: + - image: amazonlinux + name: sidecar + - image: amazonlinux + name: balajilovesoreos + env: + - name: AWS_ROLE_ARN + value: already-defined + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodWithInitContainer.pod.yaml b/pkg/handler/testdata/rawPodWithInitContainer.pod.yaml new file mode 100644 index 000000000..ab5d43ae0 --- /dev/null +++ b/pkg/handler/testdata/rawPodWithInitContainer.pod.yaml @@ -0,0 +1,20 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + uid: be8695c4-4ad0-4038-8786-c508853aa255 + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/handler/region: "seattle" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]},{"op":"add","path":"/spec/initContainers","value":[{"name":"initcontainer","image":"amazonlinux","env":[{"name":"AWS_DEFAULT_REGION","value":"seattle"},{"name":"AWS_REGION","value":"seattle"},{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + initContainers: + - image: amazonlinux + name: initcontainer + serviceAccountName: default + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/rawPodWithVolumes.pod.yaml b/pkg/handler/testdata/rawPodWithVolumes.pod.yaml new file mode 100644 index 000000000..cccb41ac6 --- /dev/null +++ b/pkg/handler/testdata/rawPodWithVolumes.pod.yaml @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + uid: be8695c4-4ad0-4038-8786-c508853aa255 + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes/0","value":{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default + volumes: + - name: my-volume diff --git a/pkg/handler/testdata/rawPodWithoutVolumes.pod.yaml b/pkg/handler/testdata/rawPodWithoutVolumes.pod.yaml new file mode 100644 index 000000000..235ae2c1f --- /dev/null +++ b/pkg/handler/testdata/rawPodWithoutVolumes.pod.yaml @@ -0,0 +1,14 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"/var/run/secrets/eks.amazonaws.com/serviceaccount/token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + serviceAccountName: default diff --git a/pkg/handler/testdata/windowsPodWithoutVolumes.pod.yaml b/pkg/handler/testdata/windowsPodWithoutVolumes.pod.yaml new file mode 100644 index 000000000..f2165bc4f --- /dev/null +++ b/pkg/handler/testdata/windowsPodWithoutVolumes.pod.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Pod +metadata: + name: balajilovesoreos + uid: be8695c4-4ad0-4038-8786-c508853aa255 + annotations: + testing.eks.amazonaws.com/skip: "false" + testing.eks.amazonaws.com/serviceAccount/roleArn: "arn:aws:iam::111122223333:role/s3-reader" + testing.eks.amazonaws.com/serviceAccount/audience: "sts.amazonaws.com" + testing.eks.amazonaws.com/expectedPatch: '[{"op":"add","path":"/spec/volumes","value":[{"name":"aws-iam-token","projected":{"sources":[{"serviceAccountToken":{"audience":"sts.amazonaws.com","expirationSeconds":86400,"path":"token"}}]}}]},{"op":"add","path":"/spec/containers","value":[{"name":"balajilovesoreos","image":"amazonlinux","env":[{"name":"AWS_ROLE_ARN","value":"arn:aws:iam::111122223333:role/s3-reader"},{"name":"AWS_WEB_IDENTITY_TOKEN_FILE","value":"C:\\var\\run\\secrets\\eks.amazonaws.com\\serviceaccount\\token"}],"resources":{},"volumeMounts":[{"name":"aws-iam-token","readOnly":true,"mountPath":"/var/run/secrets/eks.amazonaws.com/serviceaccount"}]}]}]' +spec: + containers: + - image: amazonlinux + name: balajilovesoreos + nodeSelector: + kubernetes.io/arch: amd64 + kubernetes.io/os: windows + serviceAccountName: default +