Skip to content
Open
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.24.0
require (
github.com/onsi/ginkgo v1.16.5
github.com/onsi/gomega v1.36.1
gopkg.in/yaml.v3 v3.0.1
k8s.io/api v0.33.0
k8s.io/apimachinery v0.33.0
k8s.io/client-go v0.33.0
Expand Down Expand Up @@ -53,7 +54,6 @@ require (
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.32.1 // indirect
k8s.io/gengo/v2 v2.0.0-20240911193312-2b36238f13e9 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
Expand Down
81 changes: 69 additions & 12 deletions pkg/credentials/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,40 @@ import (
"net/url"
"os"

"gopkg.in/yaml.v3"
"k8s.io/client-go/rest"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
clientcmdlatest "k8s.io/client-go/tools/clientcmd/api/latest"
"k8s.io/klog/v2"
"sigs.k8s.io/cluster-inventory-api/apis/v1alpha1"
)

// client.authentication.k8s.io/exec is a reserved extension key defined by the Kubernetes
// client authentication API (SIG Auth), not by the ClusterProfile API.
// Reference:
// https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/#client-authentication-k8s-io-v1beta1-Cluster
const clusterExtensionKey = "client.authentication.k8s.io/exec"
const (
// client.authentication.k8s.io/exec is a reserved extension key defined by the Kubernetes
// client authentication API (SIG Auth), not by the ClusterProfile API.
// Reference:
// https://kubernetes.io/docs/reference/config-api/client-authentication.v1beta1/#client-authentication-k8s-io-v1beta1-Cluster
clusterExecExtensionKey = "client.authentication.k8s.io/exec"

// additionalCLIArgsExtensionKey and additionalEnvVarsExtensionKey are
// two reserved extensions defined in KEP 5339, which allows users to pass in (usually cluster-specific)
// additional command-line arguments and environment variables to the exec plugin from
// the ClusterProfile API side.
additionalCLIArgsExtensionKey = "multicluster.x-k8s.io/clusterprofiles/auth/exec/additional-args"
additionalEnvVarsExtensionKey = "multicluster.x-k8s.io/clusterprofiles/auth/exec/additional-envs"
)

type AdditionalCLIArgEnvVarExtensionFlag int

const (
AdditionalCLIArgEnvVarExtensionFlagIgnore AdditionalCLIArgEnvVarExtensionFlag = iota
AdditionalCLIArgEnvVarExtensionFlagAllow
)

type Provider struct {
Name string `json:"name"`
ExecConfig *clientcmdapi.ExecConfig `json:"execConfig"`
Name string `json:"name"`
ExecConfig *clientcmdapi.ExecConfig `json:"execConfig"`
AdditionalCLIArgEnvVarExtensionFlag AdditionalCLIArgEnvVarExtensionFlag `json:"additionalCLIArgEnvVarExtensionFlag,omitempty"`
}

type CredentialsProvider struct {
Expand Down Expand Up @@ -68,11 +86,47 @@ func (cp *CredentialsProvider) BuildConfigFromCP(clusterprofile *v1alpha1.Cluste
}

// 2. Get Exec Config
execConfig := cp.getExecConfigFromConfig(clusterAccessor.Name)
execConfig, additionalCLIArgEnvVarsExtFlag := cp.getExecConfigAndFlagsFromConfig(clusterAccessor.Name)
if execConfig == nil {
return nil, fmt.Errorf("no exec credentials found for provider %q", clusterAccessor.Name)
}

// 3. Add additional CLI arguments and environment variables from cluster extensions if allowed.
for idx := range clusterAccessor.Cluster.Extensions {
ext := &clusterAccessor.Cluster.Extensions[idx]

switch {
case additionalCLIArgEnvVarsExtFlag == AdditionalCLIArgEnvVarExtensionFlagAllow && ext.Name == additionalCLIArgsExtensionKey:
var additionalArgs []string
if err := yaml.Unmarshal(ext.Extension.Raw, &additionalArgs); err != nil {
return nil, fmt.Errorf("failed to unmarshal additional CLI args extension: %w", err)
}
execConfig.Args = append(execConfig.Args, additionalArgs...)
case additionalCLIArgEnvVarsExtFlag == AdditionalCLIArgEnvVarExtensionFlagAllow && ext.Name == additionalEnvVarsExtensionKey:
var additionalEnvs map[string]string
if err := yaml.Unmarshal(ext.Extension.Raw, &additionalEnvs); err != nil {
return nil, fmt.Errorf("failed to unmarshal additional env vars extension: %w", err)
}

// Update the value of existing env vars.
for idx := range execConfig.Env {
env := &execConfig.Env[idx]
if _, exists := additionalEnvs[env.Name]; exists {
env.Value = additionalEnvs[env.Name]
delete(additionalEnvs, env.Name)
}
}

// Add new env vars.
for name, value := range additionalEnvs {
execConfig.Env = append(execConfig.Env, clientcmdapi.ExecEnvVar{
Name: name,
Value: value,
})
}
}
}

// 3. build resulting rest.Config
config := &rest.Config{
Host: clusterAccessor.Cluster.Server,
Expand All @@ -94,25 +148,28 @@ func (cp *CredentialsProvider) BuildConfigFromCP(clusterprofile *v1alpha1.Cluste
Env: execConfig.Env,
InteractiveMode: "Never",
ProvideClusterInfo: execConfig.ProvideClusterInfo,
Config: execConfig.Config,
}

// Propagate reserved extension into ExecCredential.Spec.Cluster.Config if present
internalCluster := clientcmdapi.NewCluster()
if err := clientcmdlatest.Scheme.Convert(&clusterAccessor.Cluster, internalCluster, nil); err != nil {
return nil, fmt.Errorf("failed to convert v1 Cluster to internal: %w", err)
}
config.ExecProvider.Config = internalCluster.Extensions[clusterExtensionKey]
if extData, ok := internalCluster.Extensions[clusterExecExtensionKey]; ok {
config.ExecProvider.Config = extData
}

return config, nil
}

func (cp *CredentialsProvider) getExecConfigFromConfig(providerName string) *clientcmdapi.ExecConfig {
func (cp *CredentialsProvider) getExecConfigAndFlagsFromConfig(providerName string) (*clientcmdapi.ExecConfig, AdditionalCLIArgEnvVarExtensionFlag) {
for _, provider := range cp.Providers {
if provider.Name == providerName {
return provider.ExecConfig
return provider.ExecConfig, provider.AdditionalCLIArgEnvVarExtensionFlag
}
}
return nil
return nil, AdditionalCLIArgEnvVarExtensionFlagIgnore
}

// getClusterAccessorFromClusterProfile returns the first AccessProvider from the ClusterProfile
Expand Down
104 changes: 99 additions & 5 deletions pkg/credentials/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@ import (

"github.com/onsi/ginkgo"
"github.com/onsi/gomega"
"gopkg.in/yaml.v3"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
clientauthenticationv1 "k8s.io/client-go/pkg/apis/clientauthentication/v1"
clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
clientcmdv1 "k8s.io/client-go/tools/clientcmd/api/v1"
Expand Down Expand Up @@ -65,6 +67,7 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
Args: []string{"arg3"},
APIVersion: "client.authentication.k8s.io/v1beta1",
},
AdditionalCLIArgEnvVarExtensionFlag: AdditionalCLIArgEnvVarExtensionFlagAllow,
},
}
credentialsProvider = New(testProviders)
Expand Down Expand Up @@ -132,6 +135,7 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
Command: "gke-gcloud-auth-plugin",
ProvideClusterInfo: true,
},
AdditionalCLIArgEnvVarExtensionFlag: AdditionalCLIArgEnvVarExtensionFlagIgnore,
},
},
}
Expand All @@ -150,6 +154,7 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
gomega.Expect(cp.Providers).To(gomega.HaveLen(1))
gomega.Expect(cp.Providers[0].Name).To(gomega.Equal("gkeFleet"))
gomega.Expect(cp.Providers[0].ExecConfig.Command).To(gomega.Equal("gke-gcloud-auth-plugin"))
gomega.Expect(cp.Providers[0].AdditionalCLIArgEnvVarExtensionFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagIgnore))
})

ginkgo.It("should return an error when file does not exist", func() {
Expand Down Expand Up @@ -185,26 +190,38 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {

ginkgo.Describe("getExecConfigFromConfig", func() {
ginkgo.It("should return the correct ExecConfig for existing provider", func() {
execConfig := credentialsProvider.getExecConfigFromConfig("test-provider-1")
execConfig, additionalCLIArgEnvVarsExtFlag := credentialsProvider.getExecConfigAndFlagsFromConfig("test-provider-1")
gomega.Expect(execConfig).NotTo(gomega.BeNil())
gomega.Expect(execConfig.Command).To(gomega.Equal("test-command-1"))
gomega.Expect(execConfig.Args).To(gomega.Equal([]string{"arg1", "arg2"}))
gomega.Expect(additionalCLIArgEnvVarsExtFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagIgnore))
})

ginkgo.It("should return the correct ExecConfig for another existing provider", func() {
execConfig, additionalCLIArgEnvVarsExtFlag := credentialsProvider.getExecConfigAndFlagsFromConfig("test-provider-2")
gomega.Expect(execConfig).NotTo(gomega.BeNil())
gomega.Expect(execConfig.Command).To(gomega.Equal("test-command-2"))
gomega.Expect(execConfig.Args).To(gomega.Equal([]string{"arg3"}))
gomega.Expect(additionalCLIArgEnvVarsExtFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagAllow))
})

ginkgo.It("should return nil for non-existing provider", func() {
execConfig := credentialsProvider.getExecConfigFromConfig("non-existent-provider")
execConfig, additionalCLIArgEnvVarsExtFlag := credentialsProvider.getExecConfigAndFlagsFromConfig("non-existent-provider")
gomega.Expect(execConfig).To(gomega.BeNil())
gomega.Expect(additionalCLIArgEnvVarsExtFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagIgnore))
})

ginkgo.It("should return nil for empty provider name", func() {
execConfig := credentialsProvider.getExecConfigFromConfig("")
execConfig, additionalCLIArgEnvVarsExtFlag := credentialsProvider.getExecConfigAndFlagsFromConfig("")
gomega.Expect(execConfig).To(gomega.BeNil())
gomega.Expect(additionalCLIArgEnvVarsExtFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagIgnore))
})

ginkgo.It("should handle CredentialsProvider with no providers", func() {
emptyCP := New([]Provider{})
execConfig := emptyCP.getExecConfigFromConfig("any-provider")
execConfig, additionalCLIArgEnvVarsExtFlag := emptyCP.getExecConfigAndFlagsFromConfig("any-provider")
gomega.Expect(execConfig).To(gomega.BeNil())
gomega.Expect(additionalCLIArgEnvVarsExtFlag).To(gomega.Equal(AdditionalCLIArgEnvVarExtensionFlagIgnore))
})
})

Expand Down Expand Up @@ -285,6 +302,8 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
ginkgo.Describe("BuildConfigFromCP", func() {
var clusterProfile *v1alpha1.ClusterProfile

additionalCLIArgsData, _ := yaml.Marshal([]string{"--audience", "audience"})
additionalEnvVarsData, _ := yaml.Marshal(map[string]string{"CLIENT_ID": "client-id", "TENANT_ID": "tenant-id"})
ginkgo.BeforeEach(func() {
clusterProfile = &v1alpha1.ClusterProfile{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -298,6 +317,26 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
Server: "https://test-server.com",
CertificateAuthorityData: []byte("test-ca-data"),
ProxyURL: "http://proxy.example.com",
Extensions: []clientcmdv1.NamedExtension{
{
Name: clusterExecExtensionKey,
Extension: runtime.RawExtension{
Raw: []byte("arbitrary-data"),
},
},
{
Name: additionalCLIArgsExtensionKey,
Extension: runtime.RawExtension{
Raw: additionalCLIArgsData,
},
},
{
Name: additionalEnvVarsExtensionKey,
Extension: runtime.RawExtension{
Raw: additionalEnvVarsData,
},
},
},
},
},
},
Expand Down Expand Up @@ -327,7 +366,7 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
gomega.Expect(err.Error()).To(gomega.ContainSubstring("no exec credentials found for provider"))
})

ginkgo.It("should build config successfully", func() {
ginkgo.It("should build config successfully (no additional CLI args/env vars)", func() {
cred := clientauthenticationv1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1",
Expand Down Expand Up @@ -355,6 +394,61 @@ var _ = ginkgo.Describe("CredentialsProvider", func() {
gomega.Expect(config).NotTo(gomega.BeNil())
gomega.Expect(config.Host).To(gomega.Equal("https://test-server.com"))
gomega.Expect(config.TLSClientConfig.CAData).To(gomega.Equal([]byte("test-ca-data")))
gomega.Expect(config.ExecProvider.Command).To(gomega.Equal("cat"))
gomega.Expect(config.ExecProvider.Args).To(gomega.Equal([]string{testFile}))
})

ginkgo.It("should build config successfully (with additional CLI args/env vars)", func() {
cred := clientauthenticationv1.ExecCredential{
TypeMeta: metav1.TypeMeta{
APIVersion: "client.authentication.k8s.io/v1",
Kind: "ExecCredential",
},
Status: &clientauthenticationv1.ExecCredentialStatus{
Token: "test-token",
},
}
jsonData, err := json.Marshal(cred)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
testFile := filepath.Join(tempDir, "test-config.json")
err = os.WriteFile(testFile, jsonData, 0644)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
execCP := New([]Provider{
{
Name: "test-provider-1",
ExecConfig: &clientcmdapi.ExecConfig{
APIVersion: "client.authentication.k8s.io/v1",
Command: "cat",
Args: []string{testFile},
Env: []clientcmdapi.ExecEnvVar{
{
Name: "CLIENT_ID",
Value: "None",
},
},
},
AdditionalCLIArgEnvVarExtensionFlag: AdditionalCLIArgEnvVarExtensionFlagAllow,
},
})

config, err := execCP.BuildConfigFromCP(clusterProfile)
gomega.Expect(err).NotTo(gomega.HaveOccurred())
gomega.Expect(config).NotTo(gomega.BeNil())
gomega.Expect(config.Host).To(gomega.Equal("https://test-server.com"))
gomega.Expect(config.TLSClientConfig.CAData).To(gomega.Equal([]byte("test-ca-data")))
gomega.Expect(config.ExecProvider.Command).To(gomega.Equal("cat"))
gomega.Expect(config.ExecProvider.Args).To(gomega.Equal([]string{testFile, "--audience", "audience"}))
gomega.Expect(len(config.ExecProvider.Env)).To(gomega.Equal(2))
gomega.Expect(config.ExecProvider.Env).To(gomega.ContainElements(
clientcmdapi.ExecEnvVar{
Name: "CLIENT_ID",
Value: "client-id",
},
clientcmdapi.ExecEnvVar{
Name: "TENANT_ID",
Value: "tenant-id",
},
))
})
})
})
File renamed without changes.
Loading