Skip to content

Commit

Permalink
Add creds provider for ECR
Browse files Browse the repository at this point in the history
  • Loading branch information
ayberk committed Nov 20, 2020
1 parent 1be8de7 commit e59fdb7
Show file tree
Hide file tree
Showing 8 changed files with 811 additions and 3 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
/aws-cloud-controller-manager
/cloudconfig

.vscode/
152 changes: 152 additions & 0 deletions cmd/ecr-credential-provider/framework/plugin.go
Original file line number Diff line number Diff line change
@@ -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 framework

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
}
101 changes: 101 additions & 0 deletions cmd/ecr-credential-provider/framework/plugin_test.go
Original file line number Diff line number Diff line change
@@ -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 framework

import (
"bytes"
"context"
"reflect"
"testing"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/kubernetes/pkg/credentialprovider/apis/credentials/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")
}
})
}
}
Loading

0 comments on commit e59fdb7

Please sign in to comment.