From ec091a5197109649a5060a5c2eba14119befaa4f Mon Sep 17 00:00:00 2001 From: Ayberk Yilmaz <309940+ayberk@users.noreply.github.com> Date: Sat, 14 Nov 2020 02:00:29 +0000 Subject: [PATCH] Add creds provider for ECR --- .gitignore | 3 + Makefile | 6 + cmd/ecr-credential-provider/main.go | 159 +++++++++++++ cmd/ecr-credential-provider/main_test.go | 250 +++++++++++++++++++++ cmd/ecr-credential-provider/plugin.go | 152 +++++++++++++ cmd/ecr-credential-provider/plugin_test.go | 101 +++++++++ go.mod | 1 + go.sum | 3 + pkg/providers/v2/mocks/mock_ecr.go | 49 ++++ 9 files changed, 724 insertions(+) create mode 100644 cmd/ecr-credential-provider/main.go create mode 100644 cmd/ecr-credential-provider/main_test.go create mode 100644 cmd/ecr-credential-provider/plugin.go create mode 100644 cmd/ecr-credential-provider/plugin_test.go create mode 100644 pkg/providers/v2/mocks/mock_ecr.go diff --git a/.gitignore b/.gitignore index f922b978d1..2e628ab8bd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ /aws-cloud-controller-manager +/ecr-credential-provider /cloudconfig + +.vscode/ diff --git a/Makefile b/Makefile index d1a6900e69..c1227e09db 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,12 @@ aws-cloud-controller-manager: $(SOURCES) -o=aws-cloud-controller-manager \ cmd/aws-cloud-controller-manager/main.go +ecr-credential-provider: $(shell find ./cmd/ecr-credential-provider -name '*.go') + GO111MODULE=on CGO_ENABLED=0 GOOS=$(GOOS) GOPROXY=$(GOPROXY) go build \ + -ldflags="-w -s -X 'main.version=$(VERSION)'" \ + -o=ecr-credential-provider \ + cmd/ecr-credential-provider/*.go + .PHONY: docker-build docker-build: docker build \ diff --git a/cmd/ecr-credential-provider/main.go b/cmd/ecr-credential-provider/main.go new file mode 100644 index 0000000000..096fa9d6be --- /dev/null +++ b/cmd/ecr-credential-provider/main.go @@ -0,0 +1,159 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "net/url" + "os" + "regexp" + "strings" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ecr" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/klog/v2" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +var ecrPattern = regexp.MustCompile(`^(\d{12})\.dkr\.ecr(\-fips)?\.([a-zA-Z0-9][a-zA-Z0-9-_]*)\.(amazonaws\.com(\.cn)?|sc2s\.sgov\.gov|c2s\.ic\.gov)$`) + +// ECR abstracts the calls we make to aws-sdk for testing purposes +type ECR interface { + GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) +} + +type ecrPlugin struct { + ecr ECR +} + +func defaultECRProvider(region string, registryID string) (*ecr.ECR, error) { + sess, err := session.NewSessionWithOptions(session.Options{ + Config: aws.Config{Region: aws.String(region)}, + SharedConfigState: session.SharedConfigEnable, + }) + if err != nil { + return nil, err + } + + return ecr.New(sess), nil +} + +func (e *ecrPlugin) GetCredentials(ctx context.Context, image string, args []string) (*v1alpha1.CredentialProviderResponse, error) { + registryID, region, registry, err := parseRepoURL(image) + if err != nil { + return nil, err + } + + if e.ecr == nil { + e.ecr, err = defaultECRProvider(region, registryID) + if err != nil { + return nil, err + } + } + + output, err := e.ecr.GetAuthorizationToken(&ecr.GetAuthorizationTokenInput{ + RegistryIds: []*string{aws.String(registryID)}, + }) + if err != nil { + return nil, err + } + + if output == nil { + return nil, errors.New("response output from ECR was nil") + } + + if len(output.AuthorizationData) == 0 { + return nil, errors.New("authorization data was empty") + } + + data := output.AuthorizationData[0] + if data.AuthorizationToken == nil { + return nil, errors.New("authorization token in response was nil") + } + + decodedToken, err := base64.StdEncoding.DecodeString(aws.StringValue(data.AuthorizationToken)) + if err != nil { + return nil, err + } + + parts := strings.SplitN(string(decodedToken), ":", 2) + if len(parts) != 2 { + return nil, errors.New("error parsing username and password from authorization token") + } + + var cacheDuration *metav1.Duration + expiresAt := data.ExpiresAt + if expiresAt == nil { + // explicitly set cache duration to 0 if expiresAt was nil so that + // kubelet does not cache it in-memory + cacheDuration = &metav1.Duration{Duration: 0} + } else { + // halving duration in order to compensate for the time loss between + // the token creation and passing it all the way to kubelet. + duration := time.Duration((expiresAt.Unix() - time.Now().Unix()) / 2) + if duration > 0 { + cacheDuration = &metav1.Duration{Duration: duration} + } + } + + return &v1alpha1.CredentialProviderResponse{ + CacheKeyType: v1alpha1.RegistryPluginCacheKeyType, + CacheDuration: cacheDuration, + Auth: map[string]v1alpha1.AuthConfig{ + registry: { + Username: parts[0], + Password: parts[1], + }, + }, + }, nil + +} + +// parseRepoURL parses and splits the registry URL +// returns (registryID, region, registry). +// .dkr.ecr(-fips)..amazonaws.com(.cn) +func parseRepoURL(image string) (string, string, string, error) { + if !strings.Contains(image, "https://") { + image = "https://" + image + } + parsed, err := url.Parse(image) + if err != nil { + return "", "", "", fmt.Errorf("error parsing image %s: %v", image, err) + } + + splitURL := ecrPattern.FindStringSubmatch(parsed.Hostname()) + if len(splitURL) < 4 { + return "", "", "", fmt.Errorf("%s is not a valid ECR repository URL", parsed.Hostname()) + } + + return splitURL[1], splitURL[3], parsed.Hostname(), nil +} + +func main() { + p := NewCredentialProvider(&ecrPlugin{}) + if err := p.Run(context.TODO()); err != nil { + klog.Errorf("Error running credential provider plugin: %v", err) + os.Exit(1) + } +} diff --git a/cmd/ecr-credential-provider/main_test.go b/cmd/ecr-credential-provider/main_test.go new file mode 100644 index 0000000000..181c400256 --- /dev/null +++ b/cmd/ecr-credential-provider/main_test.go @@ -0,0 +1,250 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "testing" + "time" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/service/ecr" + "github.com/golang/mock/gomock" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cloud-provider-aws/pkg/providers/v2/mocks" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +func generateGetAuthorizationTokenOutput(user string, password string, proxy string, expiration *time.Time) *ecr.GetAuthorizationTokenOutput { + creds := []byte(fmt.Sprintf("%s:%s", user, password)) + data := &ecr.AuthorizationData{ + AuthorizationToken: aws.String(base64.StdEncoding.EncodeToString(creds)), + ExpiresAt: expiration, + ProxyEndpoint: aws.String(proxy), + } + output := &ecr.GetAuthorizationTokenOutput{ + AuthorizationData: []*ecr.AuthorizationData{data}, + } + return output +} + +func generateResponse(registry string, username string, password string) *v1alpha1.CredentialProviderResponse { + return &v1alpha1.CredentialProviderResponse{ + CacheKeyType: v1alpha1.RegistryPluginCacheKeyType, + CacheDuration: &metav1.Duration{Duration: 0}, + Auth: map[string]v1alpha1.AuthConfig{ + registry: { + Username: username, + Password: password, + }, + }, + } +} + +func Test_GetCredentials(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockECR := mocks.NewMockECR(ctrl) + + testcases := []struct { + name string + image string + args []string + getAuthorizationTokenOutput *ecr.GetAuthorizationTokenOutput + getAuthorizationTokenError error + response *v1alpha1.CredentialProviderResponse + expectedError error + }{ + { + name: "success", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: generateGetAuthorizationTokenOutput("user", "pass", "", nil), + response: generateResponse("123456789123.dkr.ecr.us-west-2.amazonaws.com", "user", "pass"), + }, + { + name: "empty authorization data", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: &ecr.GetAuthorizationTokenOutput{}, + getAuthorizationTokenError: nil, + expectedError: errors.New("authorization data was empty"), + }, + { + name: "nil response", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: nil, + getAuthorizationTokenError: nil, + expectedError: errors.New("response output from ECR was nil"), + }, + { + name: "empty authorization token", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: &ecr.GetAuthorizationTokenOutput{AuthorizationData: []*ecr.AuthorizationData{{}}}, + getAuthorizationTokenError: nil, + expectedError: errors.New("authorization token in response was nil"), + }, + { + name: "invalid authorization token", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: nil, + getAuthorizationTokenError: errors.New("getAuthorizationToken failed"), + expectedError: errors.New("getAuthorizationToken failed"), + }, + { + name: "invalid authorization token", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + getAuthorizationTokenOutput: &ecr.GetAuthorizationTokenOutput{ + AuthorizationData: []*ecr.AuthorizationData{ + {AuthorizationToken: aws.String(base64.StdEncoding.EncodeToString([]byte(fmt.Sprint("foo"))))}, + }, + }, + getAuthorizationTokenError: nil, + expectedError: errors.New("error parsing username and password from authorization token"), + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + p := &ecrPlugin{ecr: mockECR} + mockECR.EXPECT().GetAuthorizationToken(gomock.Any()).Return(testcase.getAuthorizationTokenOutput, testcase.getAuthorizationTokenError) + + creds, err := p.GetCredentials(context.TODO(), testcase.image, testcase.args) + + if testcase.expectedError != nil && (testcase.expectedError.Error() != err.Error()) { + t.Fatalf("expected %s, got %s", testcase.expectedError.Error(), err.Error()) + } + + if testcase.expectedError == nil { + if creds.CacheKeyType != testcase.response.CacheKeyType { + t.Fatalf("Unexpected CacheKeyType. Expected: %s, got: %s", testcase.response.CacheKeyType, creds.CacheKeyType) + } + + if creds.Auth[testcase.image] != testcase.response.Auth[testcase.image] { + t.Fatalf("Unexpected Auth. Expected: %s, got: %s", testcase.response.Auth[testcase.image], creds.Auth[testcase.image]) + } + + if creds.CacheDuration.Duration != testcase.response.CacheDuration.Duration { + t.Fatalf("Unexpected CacheDuration. Expected: %s, got: %s", testcase.response.CacheDuration.Duration, creds.CacheDuration.Duration) + } + } + }) + } +} + +func Test_ParseURL(t *testing.T) { + testcases := []struct { + name string + image string + registryID string + region string + registry string + err error + }{ + { + name: "success", + image: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + registryID: "123456789123", + region: "us-west-2", + registry: "123456789123.dkr.ecr.us-west-2.amazonaws.com", + err: nil, + }, + { + name: "invalid registry", + image: "foobar", + registryID: "", + region: "", + registry: "", + err: errors.New("foobar is not a valid ECR repository URL"), + }, + { + name: "invalid URL", + image: "foobar ", + registryID: "", + region: "", + registry: "", + err: errors.New("error parsing image https://foobar : parse \"https://foobar \": invalid character \" \" in host name"), + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + registryID, region, registry, err := parseRepoURL(testcase.image) + + if testcase.err != nil && (testcase.err.Error() != err.Error()) { + t.Fatalf("expected error %s, got %s", testcase.err, err) + } + + if registryID != testcase.registryID { + t.Fatalf("registryID mismatch. Expected %s, got %s", testcase.registryID, registryID) + } + + if region != testcase.region { + t.Fatalf("region mismatch. Expected %s, got %s", testcase.region, region) + } + + if registry != testcase.registry { + t.Fatalf("registry mismatch. Expected %s, got %s", testcase.registry, registry) + } + }) + } +} + +func TestRegistryPatternMatch(t *testing.T) { + grid := []struct { + Registry string + Expected bool + }{ + {"123456789012.dkr.ecr.lala-land-1.amazonaws.com", true}, + // fips + {"123456789012.dkr.ecr-fips.lala-land-1.amazonaws.com", true}, + // .cn + {"123456789012.dkr.ecr.lala-land-1.amazonaws.com.cn", true}, + // registry ID too long + {"1234567890123.dkr.ecr.lala-land-1.amazonaws.com", false}, + // registry ID too short + {"12345678901.dkr.ecr.lala-land-1.amazonaws.com", false}, + // registry ID has invalid chars + {"12345678901A.dkr.ecr.lala-land-1.amazonaws.com", false}, + // region has invalid chars + {"123456789012.dkr.ecr.lala-land-1!.amazonaws.com", false}, + // region starts with invalid char + {"123456789012.dkr.ecr.#lala-land-1.amazonaws.com", false}, + // invalid host suffix + {"123456789012.dkr.ecr.lala-land-1.amazonaws.hacker.com", false}, + // invalid host suffix + {"123456789012.dkr.ecr.lala-land-1.hacker.com", false}, + // invalid host suffix + {"123456789012.dkr.ecr.lala-land-1.amazonaws.lol", false}, + // without dkr + {"123456789012.dog.ecr.lala-land-1.amazonaws.com", false}, + // without ecr + {"123456789012.dkr.cat.lala-land-1.amazonaws.com", false}, + // without amazonaws + {"123456789012.dkr.cat.lala-land-1.awsamazon.com", false}, + // too short + {"123456789012.lala-land-1.amazonaws.com", false}, + } + for _, g := range grid { + actual := ecrPattern.MatchString(g.Registry) + if actual != g.Expected { + t.Errorf("unexpected pattern match value, want %v for %s", g.Expected, g.Registry) + } + } +} diff --git a/cmd/ecr-credential-provider/plugin.go b/cmd/ecr-credential-provider/plugin.go new file mode 100644 index 0000000000..2a43706206 --- /dev/null +++ b/cmd/ecr-credential-provider/plugin.go @@ -0,0 +1,152 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/apimachinery/pkg/runtime/serializer/json" + "k8s.io/kubelet/pkg/apis/credentialprovider/install" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme) +) + +func init() { + install.Install(scheme) +} + +// CredentialProvider is an interface implemented by the kubelet credential provider plugin to fetch +// the username/password based on the provided image name. +type CredentialProvider interface { + GetCredentials(ctx context.Context, image string, args []string) (response *v1alpha1.CredentialProviderResponse, err error) +} + +// ExecPlugin implements the exec-based plugin for fetching credentials that is invoked by the kubelet. +type ExecPlugin struct { + plugin CredentialProvider +} + +// NewCredentialProvider returns an instance of execPlugin that fetches +// credentials based on the provided plugin implementing the CredentialProvider interface. +func NewCredentialProvider(plugin CredentialProvider) *ExecPlugin { + return &ExecPlugin{plugin} +} + +// Run executes the credential provider plugin. Required information for the plugin request (in +// the form of v1alpha1.CredentialProviderRequest) is provided via stdin from the kubelet. +// The CredentialProviderResponse, containing the username/password required for pulling +// the provided image, will be sent back to the kubelet via stdout. +func (e *ExecPlugin) Run(ctx context.Context) error { + return e.runPlugin(ctx, os.Stdin, os.Stdout, os.Args[1:]) +} + +func (e *ExecPlugin) runPlugin(ctx context.Context, r io.Reader, w io.Writer, args []string) error { + data, err := ioutil.ReadAll(r) + if err != nil { + return err + } + + gvk, err := json.DefaultMetaFactory.Interpret(data) + if err != nil { + return err + } + + if gvk.GroupVersion() != v1alpha1.SchemeGroupVersion { + return fmt.Errorf("group version %s is not supported", gvk.GroupVersion()) + } + + request, err := decodeRequest(data) + if err != nil { + return err + } + + if request.Image == "" { + return errors.New("image in plugin request was empty") + } + + response, err := e.plugin.GetCredentials(ctx, request.Image, args) + if err != nil { + return err + } + + if response == nil { + return errors.New("CredentialProviderResponse from plugin was nil") + } + + encodedResponse, err := encodeResponse(response) + if err != nil { + return err + } + + writer := bufio.NewWriter(w) + defer writer.Flush() + if _, err := writer.Write(encodedResponse); err != nil { + return err + } + + return nil +} + +func decodeRequest(data []byte) (*v1alpha1.CredentialProviderRequest, error) { + obj, gvk, err := codecs.UniversalDecoder(v1alpha1.SchemeGroupVersion).Decode(data, nil, nil) + if err != nil { + return nil, err + } + + if gvk.Kind != "CredentialProviderRequest" { + return nil, fmt.Errorf("kind was %q, expected CredentialProviderRequest", gvk.Kind) + } + + if gvk.Group != v1alpha1.GroupName { + return nil, fmt.Errorf("group was %q, expected %s", gvk.Group, v1alpha1.GroupName) + } + + request, ok := obj.(*v1alpha1.CredentialProviderRequest) + if !ok { + return nil, fmt.Errorf("unable to convert %T to *CredentialProviderRequest", obj) + } + + return request, nil +} + +func encodeResponse(response *v1alpha1.CredentialProviderResponse) ([]byte, error) { + mediaType := "application/json" + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), mediaType) + if !ok { + return nil, fmt.Errorf("unsupported media type %q", mediaType) + } + + encoder := codecs.EncoderForVersion(info.Serializer, v1alpha1.SchemeGroupVersion) + data, err := runtime.Encode(encoder, response) + if err != nil { + return nil, fmt.Errorf("failed to encode response: %v", err) + } + + return data, nil +} diff --git a/cmd/ecr-credential-provider/plugin_test.go b/cmd/ecr-credential-provider/plugin_test.go new file mode 100644 index 0000000000..456602deb2 --- /dev/null +++ b/cmd/ecr-credential-provider/plugin_test.go @@ -0,0 +1,101 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "bytes" + "context" + "reflect" + "testing" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubelet/pkg/apis/credentialprovider/v1alpha1" +) + +type fakePlugin struct { +} + +func (f *fakePlugin) GetCredentials(ctx context.Context, image string, args []string) (*v1alpha1.CredentialProviderResponse, error) { + return &v1alpha1.CredentialProviderResponse{ + CacheKeyType: v1alpha1.RegistryPluginCacheKeyType, + CacheDuration: &metav1.Duration{Duration: 10 * time.Minute}, + Auth: map[string]v1alpha1.AuthConfig{ + "*.registry.io": { + Username: "user", + Password: "password", + }, + }, + }, nil +} + +func Test_runPlugin(t *testing.T) { + testcases := []struct { + name string + in *bytes.Buffer + expectedOut []byte + expectErr bool + }{ + { + name: "successful test case", + in: bytes.NewBufferString(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`), + expectedOut: []byte(`{"kind":"CredentialProviderResponse","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","cacheKeyType":"Registry","cacheDuration":"10m0s","auth":{"*.registry.io":{"username":"user","password":"password"}}} +`), + expectErr: false, + }, + { + name: "invalid kind", + in: bytes.NewBufferString(`{"kind":"CredentialProviderFoo","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`), + expectedOut: nil, + expectErr: true, + }, + { + name: "invalid apiVersion", + in: bytes.NewBufferString(`{"kind":"CredentialProviderRequest","apiVersion":"foo.k8s.io/v1alpha1","image":"test.registry.io/foobar"}`), + expectedOut: nil, + expectErr: true, + }, + { + name: "empty image", + in: bytes.NewBufferString(`{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1alpha1","image":""}`), + expectedOut: nil, + expectErr: true, + }, + } + + for _, testcase := range testcases { + t.Run(testcase.name, func(t *testing.T) { + p := NewCredentialProvider(&fakePlugin{}) + + out := &bytes.Buffer{} + err := p.runPlugin(context.TODO(), testcase.in, out, nil) + if err != nil && !testcase.expectErr { + t.Fatal(err) + } + + if err == nil && testcase.expectErr { + t.Error("expected error but got none") + } + + if !reflect.DeepEqual(out.Bytes(), testcase.expectedOut) { + t.Logf("actual output: %v", string(out.Bytes())) + t.Logf("expected output: %v", string(testcase.expectedOut)) + t.Errorf("unexpected output") + } + }) + } +} diff --git a/go.mod b/go.mod index 2c31d90e33..e1e6edf14a 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( k8s.io/code-generator v0.20.0 k8s.io/component-base v0.20.0 k8s.io/klog/v2 v2.4.0 + k8s.io/kubelet v0.20.0 k8s.io/legacy-cloud-providers v0.20.0 k8s.io/utils v0.0.0-20201110183641-67b214c5f920 ) diff --git a/go.sum b/go.sum index e9ae0d458b..c1e1d3ab07 100644 --- a/go.sum +++ b/go.sum @@ -797,6 +797,9 @@ k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd h1:sOHNzJIkytDF6qadMNKhhDRpc6ODik8lVC6nOur7B2c= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= +k8s.io/kubelet v0.20.0 h1:vtQMgZ7B79vCUyEpoij2Oy6UmcJx0/D2lNXfViVTBMg= +k8s.io/kubelet v0.20.0/go.mod h1:lMdjO1NA+JZXSYtxb48pQmNERmC+vVIXIYkJIugVhl0= +k8s.io/kubernetes v1.20.0 h1:mnc69esJC3PJgSptxNJomGz2gBthyGLSEy18WiyRH4U= k8s.io/legacy-cloud-providers v0.20.0 h1:bF2WKO3ZWEjBvCv9RJxsxOAGrmQHWSgTiDBjK8uW2iw= k8s.io/legacy-cloud-providers v0.20.0/go.mod h1:1jEkaU7h9+b1EYdfWDBvhFAr+QpRfUjQfK+dGhxPGfA= k8s.io/utils v0.0.0-20201110183641-67b214c5f920 h1:CbnUZsM497iRC5QMVkHwyl8s2tB3g7yaSHkYPkpgelw= diff --git a/pkg/providers/v2/mocks/mock_ecr.go b/pkg/providers/v2/mocks/mock_ecr.go new file mode 100644 index 0000000000..a4d6a8b2d3 --- /dev/null +++ b/pkg/providers/v2/mocks/mock_ecr.go @@ -0,0 +1,49 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: cmd/ecr-credential-provider/main.go + +// Package mocks is a generated GoMock package. +package mocks + +import ( + ecr "github.com/aws/aws-sdk-go/service/ecr" + gomock "github.com/golang/mock/gomock" + reflect "reflect" +) + +// MockECR is a mock of ECR interface +type MockECR struct { + ctrl *gomock.Controller + recorder *MockECRMockRecorder +} + +// MockECRMockRecorder is the mock recorder for MockECR +type MockECRMockRecorder struct { + mock *MockECR +} + +// NewMockECR creates a new mock instance +func NewMockECR(ctrl *gomock.Controller) *MockECR { + mock := &MockECR{ctrl: ctrl} + mock.recorder = &MockECRMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockECR) EXPECT() *MockECRMockRecorder { + return m.recorder +} + +// GetAuthorizationToken mocks base method +func (m *MockECR) GetAuthorizationToken(input *ecr.GetAuthorizationTokenInput) (*ecr.GetAuthorizationTokenOutput, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAuthorizationToken", input) + ret0, _ := ret[0].(*ecr.GetAuthorizationTokenOutput) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAuthorizationToken indicates an expected call of GetAuthorizationToken +func (mr *MockECRMockRecorder) GetAuthorizationToken(input interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAuthorizationToken", reflect.TypeOf((*MockECR)(nil).GetAuthorizationToken), input) +}