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
4 changes: 3 additions & 1 deletion PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -588,7 +588,7 @@ References -

### Kubernetes

To integrate Kubernetes, utilize the configuration block outlined below. This block allows you to specify Kubernetes connection details either through a file path or directly by providing the encoded kubeconfig content. If both kubeconfig_file and kubeconfig_encoded are specified, the kubeconfig_encoded will take precedence. If neither is specified, the kubeconfig is resolved like kubectl: `$KUBECONFIG`, then `~/.kube/config`, then the in-cluster service account.
To integrate Kubernetes, utilize the configuration block outlined below. This block allows you to specify Kubernetes connection details either through a file path or directly by providing the encoded kubeconfig content. If both kubeconfig_file and kubeconfig_encoded are specified, the kubeconfig_encoded will take precedence. If neither is specified, the block fails unless `use_default_kubeconfig: true` is set, which resolves the kubeconfig like kubectl: `$KUBECONFIG` if set, otherwise `~/.kube/config`, otherwise the in-cluster service account.


```yaml
Expand All @@ -600,6 +600,8 @@ To integrate Kubernetes, utilize the configuration block outlined below. This bl
kubeconfig_file: path/to/kubeconfig
# Base64 encoded kubeconfig, $ cat $KUBECONFIG | base64
kubeconfig_encoded: <encoded-kubeconfig>
# Use the default kubeconfig lookup when neither kubeconfig_file nor kubeconfig_encoded is set (optional).
use_default_kubeconfig: true
# The context to use from the kubeconfig (optional). If omitted, the default is the current-context as defined in the kubeconfig.
context: <context-name>
```
Expand Down
6 changes: 5 additions & 1 deletion internal/runner/autodiscovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ var autoDiscoveryProviders = []string{"aws", "gcp", "azure", "kubernetes"}
func autoDiscoveryConfig() schema.Options {
config := make(schema.Options, 0, len(autoDiscoveryProviders))
for _, provider := range autoDiscoveryProviders {
config = append(config, schema.OptionBlock{"provider": provider})
block := schema.OptionBlock{"provider": provider}
if provider == "kubernetes" {
block["use_default_kubeconfig"] = "true"
}
config = append(config, block)
}
return config
}
Expand Down
29 changes: 29 additions & 0 deletions internal/runner/autodiscovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,32 @@ func TestNewInventoryFailsFastWithoutAutoDiscovery(t *testing.T) {
})
require.Error(t, err)
}

func TestAutoDiscoveryKubernetesUsesDefaultKubeconfig(t *testing.T) {
kubeconfig := filepath.Join(t.TempDir(), "config")
require.NoError(t, os.WriteFile(kubeconfig, []byte(`apiVersion: v1
kind: Config
clusters:
- name: test
cluster:
server: https://k8s.example.test:6443
contexts:
- name: test
context:
cluster: test
current-context: test
`), 0o600))
t.Setenv("KUBERNETES_SERVICE_HOST", "")
t.Setenv("KUBECONFIG", kubeconfig)

var kubernetes schema.Options
for _, block := range autoDiscoveryConfig() {
if block["provider"] == "kubernetes" {
kubernetes = append(kubernetes, block)
}
}
r := &Runner{options: &Options{AutoDiscovery: true}}
inv, err := r.newInventory(kubernetes)
require.NoError(t, err)
require.Len(t, inv.Providers, 1)
}
30 changes: 22 additions & 8 deletions pkg/providers/k8s/kubernetes.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,22 @@ type Provider struct {
}

const (
kubeconfig_file = "kubeconfig_file"
encodedKubeConfig = "kubeconfig_encoded"
providerName = "kubernetes"
kubeconfig_file = "kubeconfig_file"
encodedKubeConfig = "kubeconfig_encoded"
useDefaultKubeconfig = "use_default_kubeconfig"
providerName = "kubernetes"
)

func New(options schema.OptionBlock) (*Provider, error) {
id, _ := options.GetMetadata("id")

configFile, _ := options.GetMetadata(kubeconfig_file)
configFile, fileOk := options.GetMetadata(kubeconfig_file)
configEncoded, strOk := options.GetMetadata(encodedKubeConfig)
// Falling back to the ambient kubeconfig must be explicit, otherwise an
// incomplete block silently enumerates whatever cluster the host can reach.
if useDefault, _ := options.GetMetadata(useDefaultKubeconfig); !fileOk && !strOk && useDefault != "true" {
return nil, errkit.New("no kubeconfig_file or kubeconfig_encoded provided, set use_default_kubeconfig to use the default kubeconfig")
}
context, _ := options.GetMetadata("context")

var kubeConfig *rest.Config
Expand Down Expand Up @@ -107,16 +113,24 @@ func (p *Provider) Resources(ctx context.Context) (*schema.Resources, error) {
return finalList, nil
}

// buildConfigWithContext resolves the kubeconfig like kubectl when no path is
// given: $KUBECONFIG, then ~/.kube/config, then the in-cluster service account.
// buildConfigWithContext loads only kubeconfigPath when set. Without a path it
// resolves like kubectl: $KUBECONFIG if set, otherwise ~/.kube/config,
// otherwise the in-cluster service account.
func buildConfigWithContext(context string, kubeconfigPath string) (*rest.Config, error) {
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
loadingRules.ExplicitPath = kubeconfigPath
loadingRules := &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfigPath}
if kubeconfigPath == "" {
loadingRules = clientcmd.NewDefaultClientConfigLoadingRules()
// The legacy ~/.kube/.kubeconfig migration writes to the home directory.
loadingRules.MigrationRules = nil
}
kubeConfig, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(
loadingRules,
&clientcmd.ConfigOverrides{
CurrentContext: context,
}).ClientConfig()
if kubeconfigPath == "" && clientcmd.IsEmptyConfig(err) {
return nil, errkit.New("no kubeconfig found in $KUBECONFIG, ~/.kube/config or the in-cluster service account")
}
if err != nil {
return nil, errkit.Wrap(err, "could not read kubeconfig file")
}
Expand Down
78 changes: 58 additions & 20 deletions pkg/providers/k8s/kubernetes_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package k8s

import (
"fmt"
"os"
"path/filepath"
"testing"
Expand All @@ -12,41 +13,78 @@ import (
const testKubeconfig = `apiVersion: v1
kind: Config
clusters:
- name: test
- name: one
cluster:
server: https://one.example.test:6443
- name: two
cluster:
server: https://k8s.example.test:6443
server: https://two.example.test:6443
users:
- name: test
user:
token: test
contexts:
- name: test
- name: one
context:
cluster: test
cluster: one
user: test
current-context: test
- name: two
context:
cluster: two
user: test
current-context: %s
`

func isolateKubeconfig(t *testing.T, kubeconfig string) {
func writeKubeconfig(t *testing.T, currentContext string) string {
t.Helper()
t.Setenv("HOME", t.TempDir())
t.Setenv("KUBERNETES_SERVICE_HOST", "")
t.Setenv("KUBECONFIG", kubeconfig)
path := filepath.Join(t.TempDir(), "config")
require.NoError(t, os.WriteFile(path, []byte(fmt.Sprintf(testKubeconfig, currentContext)), 0o600))
return path
}

func TestNewWithoutKubeconfigUsesEnvironment(t *testing.T) {
path := filepath.Join(t.TempDir(), "config")
require.NoError(t, os.WriteFile(path, []byte(testKubeconfig), 0o600))
isolateKubeconfig(t, path)
func host(t *testing.T, p *Provider) string {
t.Helper()
return p.clientSet.CoreV1().RESTClient().Get().URL().Host
}

p, err := New(schema.OptionBlock{})
require.NoError(t, err)
require.Equal(t, "k8s.example.test:6443", p.clientSet.CoreV1().RESTClient().Get().URL().Host)
func TestNew(t *testing.T) {
envConfig := writeKubeconfig(t, "one")
fileConfig := writeKubeconfig(t, "two")

tests := []struct {
name string
options schema.OptionBlock
host string
err string
}{
{name: "no kubeconfig fails without opt-in", options: schema.OptionBlock{}, err: "no kubeconfig_file or kubeconfig_encoded provided"},
{name: "empty kubeconfig_file fails without opt-in", options: schema.OptionBlock{"kubeconfig_file": ""}, err: "no kubeconfig_file or kubeconfig_encoded provided"},
{name: "opt-in uses KUBECONFIG", options: schema.OptionBlock{"use_default_kubeconfig": "true"}, host: "one.example.test:6443"},
{name: "opt-in honours context", options: schema.OptionBlock{"use_default_kubeconfig": "true", "context": "two"}, host: "two.example.test:6443"},
{name: "explicit file ignores KUBECONFIG", options: schema.OptionBlock{"kubeconfig_file": fileConfig}, host: "two.example.test:6443"},
{name: "explicit file honours context", options: schema.OptionBlock{"kubeconfig_file": fileConfig, "context": "one"}, host: "one.example.test:6443"},
{name: "missing explicit file fails", options: schema.OptionBlock{"kubeconfig_file": filepath.Join(t.TempDir(), "missing")}, err: "could not read kubeconfig file"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("KUBERNETES_SERVICE_HOST", "")
t.Setenv("KUBECONFIG", envConfig)

p, err := New(tt.options)
if tt.err != "" {
require.ErrorContains(t, err, tt.err)
return
}
require.NoError(t, err)
require.Equal(t, tt.host, host(t, p))
})
}
}

func TestNewWithoutKubeconfigFailsWhenNoneFound(t *testing.T) {
isolateKubeconfig(t, filepath.Join(t.TempDir(), "missing"))
func TestNewDefaultKubeconfigNotFound(t *testing.T) {
t.Setenv("KUBERNETES_SERVICE_HOST", "")
t.Setenv("KUBECONFIG", filepath.Join(t.TempDir(), "missing"))

_, err := New(schema.OptionBlock{})
require.Error(t, err)
_, err := New(schema.OptionBlock{"use_default_kubeconfig": "true"})
require.ErrorContains(t, err, "no kubeconfig found")
}
Loading