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
8 changes: 8 additions & 0 deletions controller/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,14 @@ func (c *liveStateCache) getCluster(cluster *appv1.Cluster) (clustercache.Cluste
clustercache.SetEventProcessingInterval(clusterCacheEventsProcessingInterval),
}

if cluster.Config.KubeConfigExecProvider != nil {
clusterCacheOpts = append(clusterCacheOpts,
clustercache.SetConfigProvider(func() (*rest.Config, error) {
return cluster.RESTConfig()
}),
)
}

clusterCache = clustercache.NewClusterCache(clusterCacheConfig, clusterCacheOpts...)

_ = clusterCache.OnResourceUpdated(func(newRes *clustercache.Resource, oldRes *clustercache.Resource, namespaceResources map[kube.ResourceKey]*clustercache.Resource) {
Expand Down
17 changes: 12 additions & 5 deletions gitops-engine/pkg/cache/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,10 +208,10 @@ func NewClusterCache(config *rest.Config, opts ...UpdateSettingsFunc) *clusterCa
eventHandlers: map[uint64]OnEventHandler{},
processEventsHandlers: map[uint64]OnProcessEventsHandler{},
log: log,
listRetryLimit: 1,
listRetryUseBackoff: false,
listRetryFunc: ListRetryFuncNever,
parentUIDToChildren: make(map[types.UID][]kube.ResourceKey),
listRetryLimit: 1,
listRetryUseBackoff: false,
listRetryFunc: ListRetryFuncNever,
parentUIDToChildren: make(map[types.UID][]kube.ResourceKey),
}
for i := range opts {
opts[i](cache)
Expand Down Expand Up @@ -256,6 +256,7 @@ type clusterCache struct {
kubectl kube.Kubectl
log logr.Logger
config *rest.Config
configProvider func() (*rest.Config, error)
namespaces []string
clusterResources bool
settings Settings
Expand Down Expand Up @@ -521,7 +522,6 @@ func (c *clusterCache) rebuildParentToChildrenIndex() {
}
}


// addToParentUIDToChildren adds a child to the parent-to-children index
func (c *clusterCache) addToParentUIDToChildren(parentUID types.UID, childKey kube.ResourceKey) {
// Check if child is already in the list to avoid duplicates
Expand Down Expand Up @@ -1006,6 +1006,13 @@ func (c *clusterCache) sync() error {
c.resources = make(map[kube.ResourceKey]*Resource)
c.namespacedResources = make(map[schema.GroupKind]bool)
c.parentUIDToChildren = make(map[types.UID][]kube.ResourceKey)
if c.configProvider != nil {
freshConfig, err := c.configProvider()
if err != nil {
return fmt.Errorf("failed to refresh cluster config: %w", err)
}
c.config = freshConfig
}
config := c.config
version, err := c.kubectl.GetServerVersion(config)
if err != nil {
Expand Down
10 changes: 10 additions & 0 deletions gitops-engine/pkg/cache/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ func SetConfig(config *rest.Config) UpdateSettingsFunc {
}
}

// SetConfigProvider sets a callback that produces a fresh rest.Config on every cache sync.
// This is used when cluster credentials are fetched dynamically (e.g. via KubeConfigExecProvider),
// ensuring that reconnections pick up rotated CA certificates and credentials.
// If not set, the static config passed to NewClusterCache is used.
func SetConfigProvider(provider func() (*rest.Config, error)) UpdateSettingsFunc {
return func(cache *clusterCache) {
cache.configProvider = provider
}
}

// SetListPageSize sets the page size for list pager.
func SetListPageSize(listPageSize int64) UpdateSettingsFunc {
return func(cache *clusterCache) {
Expand Down
22 changes: 22 additions & 0 deletions gitops-engine/pkg/cache/settings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,25 @@ func TestSetEventsProcessingInterval(t *testing.T) {
cache.Invalidate(SetEventProcessingInterval(interval))
assert.Equal(t, interval, cache.eventProcessingInterval)
}

func TestSetConfigProvider(t *testing.T) {
cache := NewClusterCache(&rest.Config{Host: "https://original"}, SetKubectl(&kubetest.MockKubectlCmd{}))
assert.Nil(t, cache.configProvider)

provider := func() (*rest.Config, error) {
return &rest.Config{Host: "https://refreshed"}, nil
}
cache.Invalidate(SetConfigProvider(provider))

assert.NotNil(t, cache.configProvider)
refreshedConfig, err := cache.configProvider()
assert.NoError(t, err)
assert.Equal(t, "https://refreshed", refreshedConfig.Host)
}

func TestSetConfigProvider_NilByDefault(t *testing.T) {
cache := NewClusterCache(&rest.Config{Host: "https://original"}, SetKubectl(&kubetest.MockKubectlCmd{}))
assert.Nil(t, cache.configProvider)
// Config should remain unchanged when no provider is set
assert.Equal(t, "https://original", cache.config.Host)
}
90 changes: 90 additions & 0 deletions pkg/apis/application/v1alpha1/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"reflect"
"regexp"
Expand Down Expand Up @@ -2413,6 +2414,29 @@ type ExecProviderConfig struct {
InstallHint string `json:"installHint,omitempty" protobuf:"bytes,5,opt,name=installHint"`
}

// KubeConfigExecProvider is config used to call an external command to produce a full kubeconfig file.
// Unlike ExecProviderConfig which only produces credentials, this command produces an entire
// kubeconfig (server, CA, auth) which ArgoCD reads to build a REST config. ArgoCD creates a
// temp file and passes its path as the last argument to the command. The command must write a
// valid kubeconfig to that file.
type KubeConfigExecProvider struct {
// Command to execute
Command string `json:"command" protobuf:"bytes,1,opt,name=command"`

// Arguments to pass to the command when executing it.
// The temp file path is appended as the last argument automatically.
Args []string `json:"args,omitempty" protobuf:"bytes,2,rep,name=args"`

// Env defines additional environment variables to expose to the process
Env map[string]string `json:"env,omitempty" protobuf:"bytes,3,opt,name=env"`

// APIVersion is the preferred api version of the kubeconfig output
APIVersion string `json:"apiVersion,omitempty" protobuf:"bytes,4,opt,name=apiVersion"`

// InstallHint is shown to the user when the executable doesn't seem to be present
InstallHint string `json:"installHint,omitempty" protobuf:"bytes,5,opt,name=installHint"`
}

// ClusterConfig is the configuration attributes. This structure is subset of the go-client
// rest.Config with annotations added for marshalling.
type ClusterConfig struct {
Expand All @@ -2439,6 +2463,14 @@ type ClusterConfig struct {

// ProxyURL is the URL to the proxy to be used for all requests send to the server
ProxyUrl string `json:"proxyUrl,omitempty" protobuf:"bytes,8,opt,name=proxyUrl"` //nolint:revive //FIXME(var-naming)

// KubeConfigExecProvider contains configuration for an external command that produces a full kubeconfig file.
// When set, this takes precedence over all other authentication methods.
KubeConfigExecProvider *KubeConfigExecProvider `json:"kubeConfigExecProvider,omitempty" protobuf:"bytes,9,opt,name=kubeConfigExecProvider"`

// KubeConfigContext specifies the context name to use from the kubeconfig produced by KubeConfigExec.
// If empty, the default context from the kubeconfig is used.
KubeConfigContext string `json:"kubeConfigContext,omitempty" protobuf:"bytes,10,opt,name=kubeConfigContext"`
}

// TLSClientConfig contains settings to enable transport layer security
Expand Down Expand Up @@ -3700,6 +3732,62 @@ func ParseProxyUrl(proxyUrl string) (*url.URL, error) { //nolint:revive //FIXME(
return u, nil
}

// runKubeConfigExecProvider executes an external command that produces a full kubeconfig file.
// It creates a temp file, appends its path as the last argument to the command, runs the command,
// and loads the resulting kubeconfig to build a rest.Config. The server URL is overridden with
// the provided serverURL to preserve ArgoCD's cluster identity model.
func runKubeConfigExecProvider(provider *KubeConfigExecProvider, kubeConfigContext string, serverURL string) (*rest.Config, error) {
tmpFile, err := os.CreateTemp("", "argocd-kubeconfig-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp file for kubeconfig: %w", err)
}
tmpPath := tmpFile.Name()
tmpFile.Close()
defer os.Remove(tmpPath)

args := make([]string, len(provider.Args)+1)
copy(args, provider.Args)
args[len(provider.Args)] = tmpPath

cmd := exec.Command(provider.Command, args...)
cmd.Env = os.Environ()
for k, v := range provider.Env {
cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", k, v))
}

var stderr bytes.Buffer
cmd.Stderr = &stderr

if err := cmd.Run(); err != nil {
errMsg := fmt.Sprintf("kubeconfig exec provider command %q failed: %v", provider.Command, err)
if stderr.Len() > 0 {
errMsg += fmt.Sprintf(", stderr: %s", stderr.String())
}
if provider.InstallHint != "" {
errMsg += fmt.Sprintf(" (install hint: %s)", provider.InstallHint)
}
return nil, fmt.Errorf("%s", errMsg)
}

loadingRules := &clientcmd.ClientConfigLoadingRules{
ExplicitPath: tmpPath,
}
overrides := &clientcmd.ConfigOverrides{}
if kubeConfigContext != "" {
overrides.CurrentContext = kubeConfigContext
}

clientConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides)
config, err := clientConfig.ClientConfig()
if err != nil {
return nil, fmt.Errorf("failed to load kubeconfig from exec provider output: %w", err)
}

config.Host = serverURL

return config, nil
}

// RawRestConfig returns a go-client REST config from cluster that might be serialized into the file using kube.WriteKubeConfig method.
func (c *Cluster) RawRestConfig() (*rest.Config, error) {
var config *rest.Config
Expand Down Expand Up @@ -3737,6 +3825,8 @@ func (c *Cluster) RawRestConfig() (*rest.Config, error) {
CAData: c.Config.CAData,
}
switch {
case c.Config.KubeConfigExecProvider != nil:
config, err = runKubeConfigExecProvider(c.Config.KubeConfigExecProvider, c.Config.KubeConfigContext, c.Server)
case c.Config.AWSAuthConfig != nil:
args := []string{"aws", "--cluster-name", c.Config.AWSAuthConfig.ClusterName}
if c.Config.AWSAuthConfig.RoleARN != "" {
Expand Down
Loading