diff --git a/internal/credentialprovider/plugin.go b/internal/credentialprovider/plugin.go index 7734d52..7fcd47d 100644 --- a/internal/credentialprovider/plugin.go +++ b/internal/credentialprovider/plugin.go @@ -11,6 +11,9 @@ import ( "path/filepath" "time" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" + "k8s.io/kubelet/pkg/apis/credentialprovider/install" credentialproviderv1 "k8s.io/kubelet/pkg/apis/credentialprovider/v1" "sigs.k8s.io/yaml" ) @@ -48,6 +51,15 @@ type pluginProviderWrapper struct { args []string } +var ( + scheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(scheme) +) + +func init() { + install.Install(scheme) +} + // NewPluginKeyring creates a new keyring that uses credential provider plugins. func NewPluginKeyring(logger *slog.Logger, configPath, binDir string) (*PluginKeyring, error) { if configPath == "" || binDir == "" { @@ -148,13 +160,23 @@ func (kr *PluginKeyring) matchesImage(patterns []string, image string) bool { return false } -// execPlugin executes the credential provider plugin and parses the response. +// marshalRequest serializes the request in a format which contains the required apiVersion and kind fields. +func marshalRequest(request *credentialproviderv1.CredentialProviderRequest) ([]byte, error) { + jsonMediaType := "application/json" + info, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), jsonMediaType) + if !ok { + return nil, fmt.Errorf("unsupported media type %q", jsonMediaType) + } + return runtime.Encode(codecs.EncoderForVersion(info.Serializer, credentialproviderv1.SchemeGroupVersion), request) +} + +// execPlugin executes the credential provider plugin and parses the responseFile. func (kr *PluginKeyring) execPlugin(ctx context.Context, provider pluginProviderWrapper, image string) (DockerConfig, error) { // Prepare the request request := credentialproviderv1.CredentialProviderRequest{ Image: image, } - requestJSON, err := json.Marshal(request) + requestJSON, err := marshalRequest(&request) if err != nil { return nil, fmt.Errorf("failed to marshal request: %w", err) } diff --git a/internal/credentialprovider/plugin_test.go b/internal/credentialprovider/plugin_test.go new file mode 100644 index 0000000..f4f7574 --- /dev/null +++ b/internal/credentialprovider/plugin_test.go @@ -0,0 +1,68 @@ +package credentialprovider + +import ( + "github.com/neilotoole/slogt" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "os" + "path" + "testing" +) + +func TestPluginKeyring_execPlugin(t *testing.T) { + tests := map[string]struct { + requestFile string + responseFile string + exitCode string + + want DockerConfig + wantErr bool + }{ + "empty-success": { + requestFile: "basic-request.json", + responseFile: "basic-response.json", + exitCode: "0", + + want: DockerConfig{ + "foo": { + Username: "user", + Password: "not-a-secret", + Email: "", + Provider: nil, + }, + }, + wantErr: false, + }, + "empty-error": { + requestFile: "basic-request.json", + responseFile: "basic-response.json", + exitCode: "1", + + wantErr: true, + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + actualRequestFile := path.Join(t.TempDir(), tt.requestFile) + provider := pluginProviderWrapper{ + name: "fake_plugin", + binPath: "test_data/fake_plugin", + matchImages: []string{"*"}, + // fake_plugin's args are: path to save stdin to, path to copy to stdout, exit code + args: []string{actualRequestFile, path.Join("test_data", tt.responseFile), tt.exitCode}, + } + kr := &PluginKeyring{logger: slogt.New(t)} + got, err := kr.execPlugin(t.Context(), provider, "foo-image") + if (err != nil) != tt.wantErr { + t.Errorf("execPlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + actualRequest, err := os.ReadFile(actualRequestFile) + require.NoError(t, err) + expectedRequest, err := os.ReadFile(path.Join("test_data", tt.requestFile)) + require.NoError(t, err) + assert.Equal(t, string(expectedRequest), string(actualRequest)) + assert.Equal(t, tt.want, got) + }) + } +} diff --git a/internal/credentialprovider/test_data/basic-request.json b/internal/credentialprovider/test_data/basic-request.json new file mode 100644 index 0000000..841fa3e --- /dev/null +++ b/internal/credentialprovider/test_data/basic-request.json @@ -0,0 +1 @@ +{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1","image":"foo-image"} diff --git a/internal/credentialprovider/test_data/basic-response.json b/internal/credentialprovider/test_data/basic-response.json new file mode 100644 index 0000000..b96e678 --- /dev/null +++ b/internal/credentialprovider/test_data/basic-response.json @@ -0,0 +1,10 @@ +{ + "apiVersion": "credentialprovider.kubelet.k8s.io/v1", + "kind": "CredentialProviderResponse", + "auth": { + "foo": { + "username": "user", + "password": "not-a-secret" + } + } +} diff --git a/internal/credentialprovider/test_data/fake_plugin b/internal/credentialprovider/test_data/fake_plugin new file mode 100755 index 0000000..51ea380 --- /dev/null +++ b/internal/credentialprovider/test_data/fake_plugin @@ -0,0 +1,4 @@ +#!/bin/sh +cat > "$1" +cat "$2" +exit "$3"