From 86e6d74f6f07690def4de9b4e452571eb1072852 Mon Sep 17 00:00:00 2001 From: Marcin Owsiany Date: Wed, 10 Jun 2026 09:32:26 +0200 Subject: [PATCH 1/2] fix: properly format credential plugin request --- internal/credentialprovider/plugin.go | 26 ++++++- internal/credentialprovider/plugin_test.go | 68 +++++++++++++++++++ .../test_data/basic-request.json | 1 + .../test_data/basic-response.json | 10 +++ .../credentialprovider/test_data/fake_plugin | 4 ++ 5 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 internal/credentialprovider/plugin_test.go create mode 100644 internal/credentialprovider/test_data/basic-request.json create mode 100644 internal/credentialprovider/test_data/basic-response.json create mode 100755 internal/credentialprovider/test_data/fake_plugin 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..5981dc2 --- /dev/null +++ b/internal/credentialprovider/test_data/fake_plugin @@ -0,0 +1,4 @@ +#!/usr/bin/bash +cat > "$1" +cat "$2" +exit "$3" From 74b87c5c183445661f3b81c9c60ed5b96b69867c Mon Sep 17 00:00:00 2001 From: Marcin Owsiany Date: Wed, 10 Jun 2026 09:48:28 +0200 Subject: [PATCH 2/2] more portable shell path --- internal/credentialprovider/test_data/fake_plugin | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/credentialprovider/test_data/fake_plugin b/internal/credentialprovider/test_data/fake_plugin index 5981dc2..51ea380 100755 --- a/internal/credentialprovider/test_data/fake_plugin +++ b/internal/credentialprovider/test_data/fake_plugin @@ -1,4 +1,4 @@ -#!/usr/bin/bash +#!/bin/sh cat > "$1" cat "$2" exit "$3"