Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions internal/credentialprovider/plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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 == "" {
Expand Down Expand Up @@ -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)
}
Expand Down
68 changes: 68 additions & 0 deletions internal/credentialprovider/plugin_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
}
1 change: 1 addition & 0 deletions internal/credentialprovider/test_data/basic-request.json
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{"kind":"CredentialProviderRequest","apiVersion":"credentialprovider.kubelet.k8s.io/v1","image":"foo-image"}
10 changes: 10 additions & 0 deletions internal/credentialprovider/test_data/basic-response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"apiVersion": "credentialprovider.kubelet.k8s.io/v1",
"kind": "CredentialProviderResponse",
"auth": {
"foo": {
"username": "user",
"password": "not-a-secret"
}
}
}
4 changes: 4 additions & 0 deletions internal/credentialprovider/test_data/fake_plugin
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/bin/sh
cat > "$1"
cat "$2"
exit "$3"
Loading