diff --git a/cli/go.mod b/cli/go.mod index a27974d..6295fd8 100644 --- a/cli/go.mod +++ b/cli/go.mod @@ -105,7 +105,6 @@ require ( google.golang.org/grpc v1.79.1 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // indirect k8s.io/component-base v0.35.0 // indirect diff --git a/cli/go.sum b/cli/go.sum index 46b1372..3a6d5d1 100644 --- a/cli/go.sum +++ b/cli/go.sum @@ -206,7 +206,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -273,8 +272,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cli/internal/aks/deploy/cilium.go b/cli/internal/aks/deploy/cilium.go index 46ca558..dc00d3d 100644 --- a/cli/internal/aks/deploy/cilium.go +++ b/cli/internal/aks/deploy/cilium.go @@ -3,10 +3,12 @@ package deploy import ( "context" "errors" + "log" "os" "os/exec" "github.com/Azure/aks-flex/plugin/pkg/util/config" + "github.com/Azure/aks-flex/plugin/pkg/util/k8s" ) var ciliumInstallInstruction = errors.New( @@ -28,18 +30,33 @@ func deployCilium( kubeconfigFile string, cfg *config.Config, ) error { + clusterContext := cfg.ClusterName + "-admin" + k8sServiceHost, k8sServicePort, err := k8s.APIServerFromKubeconfigFile(kubeconfigFile, clusterContext) + if err != nil { + return err + } + cmd := exec.CommandContext( ctx, "cilium", "install", - "--set", "azure.resourceGroup="+cfg.ResourceGroupName, + "--kubeconfig", kubeconfigFile, + "--context", clusterContext, + "--namespace", "kube-system", + "--datapath-mode", "aks-byocni", + "--helm-set", "aksbyocni.enabled=true", + "--helm-set", "cluster.name="+cfg.ClusterName, + "--helm-set", "operator.replicas=1", + "--helm-set", "kubeProxyReplacement=true", + "--helm-set", "k8sServiceHost="+k8sServiceHost, + "--helm-set", "k8sServicePort="+k8sServicePort, ) cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr cmd.Env = append( - cmd.Env, + cmd.Environ(), "KUBECONFIG="+kubeconfigFile, - "PATH="+os.Getenv("PATH"), // so cilium can find other tools ) + log.Printf("Running: cilium install --kubeconfig %s --context %s --namespace kube-system --datapath-mode aks-byocni --helm-set aksbyocni.enabled=true --helm-set cluster.name=%s --helm-set operator.replicas=1 --helm-set kubeProxyReplacement=true --helm-set k8sServiceHost=%s --helm-set k8sServicePort=%s", kubeconfigFile, clusterContext, cfg.ClusterName, k8sServiceHost, k8sServicePort) return cmd.Run() } diff --git a/cli/internal/aks/deploy/cilium_test.go b/cli/internal/aks/deploy/cilium_test.go new file mode 100644 index 0000000..c8706fb --- /dev/null +++ b/cli/internal/aks/deploy/cilium_test.go @@ -0,0 +1,44 @@ +package deploy + +import ( + "os" + "path/filepath" + "testing" + + k8sutil "github.com/Azure/aks-flex/plugin/pkg/util/k8s" +) + +func TestKubeconfigAPIServer(t *testing.T) { + dir := t.TempDir() + kubeconfig := filepath.Join(dir, "config") + if err := os.WriteFile(kubeconfig, []byte(`apiVersion: v1 +kind: Config +current-context: test +clusters: +- name: test-cluster + cluster: + server: https://example.hcp.eastus2.azmk8s.io:443 +contexts: +- name: test + context: + cluster: test-cluster + user: test-user +users: +- name: test-user + user: + token: test +`), 0o600); err != nil { + t.Fatalf("write kubeconfig: %v", err) + } + + host, port, err := k8sutil.APIServerFromKubeconfigFile(kubeconfig, "") + if err != nil { + t.Fatalf("APIServerFromKubeconfigFile returned error: %v", err) + } + if host != "example.hcp.eastus2.azmk8s.io" { + t.Fatalf("unexpected host %q", host) + } + if port != "443" { + t.Fatalf("unexpected port %q", port) + } +} diff --git a/cli/internal/config/configcmd/defaults.go b/cli/internal/config/configcmd/defaults.go index c9dc067..d366141 100644 --- a/cli/internal/config/configcmd/defaults.go +++ b/cli/internal/config/configcmd/defaults.go @@ -42,7 +42,11 @@ func OrPlaceholder(val string) string { // reachable or the required environment variables are not set, it falls back // to placeholder values that the user must replace manually. func DefaultKubeadmConfig(ctx context.Context) *kubeadm.Config { - credentials, err := azidentity.NewAzureCLICredential(nil) + credOptions := &azidentity.AzureCLICredentialOptions{} + if tenantID := config.AzureTenantID(); tenantID != "" { + credOptions.TenantID = tenantID + } + credentials, err := azidentity.NewAzureCLICredential(credOptions) if err != nil { fmt.Fprintf(os.Stderr, "Warning: could not obtain Azure CLI credentials: %v\n", err) fmt.Fprintln(os.Stderr, "Using placeholder values — edit the output before applying.") diff --git a/cli/internal/config/configcmd/defaults_test.go b/cli/internal/config/configcmd/defaults_test.go new file mode 100644 index 0000000..1ab3551 --- /dev/null +++ b/cli/internal/config/configcmd/defaults_test.go @@ -0,0 +1 @@ +package configcmd diff --git a/karpenter/go.mod b/karpenter/go.mod index 477dee7..16481eb 100644 --- a/karpenter/go.mod +++ b/karpenter/go.mod @@ -178,7 +178,6 @@ require ( gopkg.in/dnaeon/go-vcr.v3 v3.2.0 // indirect gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/ini.v1 v1.67.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/apiextensions-apiserver v0.35.0 // indirect k8s.io/cloud-provider v0.35.0 // indirect diff --git a/karpenter/go.sum b/karpenter/go.sum index 1bad2d2..90086c9 100644 --- a/karpenter/go.sum +++ b/karpenter/go.sum @@ -480,8 +480,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugin/go.mod b/plugin/go.mod index 63852a3..3f53b2e 100644 --- a/plugin/go.mod +++ b/plugin/go.mod @@ -21,7 +21,6 @@ require ( golang.org/x/crypto v0.47.0 google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 - gopkg.in/ini.v1 v1.67.1 k8s.io/api v0.35.1 k8s.io/apimachinery v0.35.1 k8s.io/client-go v0.35.1 diff --git a/plugin/go.sum b/plugin/go.sum index 619acde..e02c2ec 100644 --- a/plugin/go.sum +++ b/plugin/go.sum @@ -197,7 +197,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -264,8 +263,6 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/ini.v1 v1.67.1 h1:tVBILHy0R6e4wkYOn3XmiITt/hEVH4TFMYvAX2Ytz6k= -gopkg.in/ini.v1 v1.67.1/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/plugin/pkg/util/config/config.go b/plugin/pkg/util/config/config.go index 7da7d0b..92f7e2c 100644 --- a/plugin/pkg/util/config/config.go +++ b/plugin/pkg/util/config/config.go @@ -3,12 +3,10 @@ package config import ( "fmt" "os" - "path/filepath" + "os/exec" "regexp" "strconv" "strings" - - "gopkg.in/ini.v1" ) var ( @@ -102,22 +100,26 @@ func (c *Config) validate() error { return nil } +// AzureTenantID returns the tenant ID of the current Azure CLI account by +// running `az account show --query 'tenantId' -o tsv`. +func AzureTenantID() string { + out, err := exec.Command("az", "account", "show", "--query", "tenantId", "-o", "tsv").Output() + if err != nil { + return "" + } + return strings.TrimSpace(string(out)) +} + func defaultSubscriptionID() string { if subscriptionID := os.Getenv("AZURE_SUBSCRIPTION_ID"); subscriptionID != "" { return subscriptionID } - b, err := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".azure/clouds.config")) + out, err := exec.Command("az", "account", "show", "--query", "id", "-o", "tsv").Output() if err != nil { return "" } - - f, err := ini.Load(b) - if err != nil { - return "" - } - - return f.Section("AzureCloud").Key("subscription").String() + return strings.TrimSpace(string(out)) } func defaultResourceGroupName() string { diff --git a/plugin/pkg/util/config/config_test.go b/plugin/pkg/util/config/config_test.go new file mode 100644 index 0000000..b4b295a --- /dev/null +++ b/plugin/pkg/util/config/config_test.go @@ -0,0 +1,44 @@ +package config + +import ( + "os" + "path/filepath" + "runtime" + "testing" +) + +func TestDefaultSubscriptionIDUsesAZCLI(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping shell-script fake az on Windows") + } + t.Setenv("AZURE_SUBSCRIPTION_ID", "") + + dir := t.TempDir() + fakeAZ := filepath.Join(dir, "az") + if err := os.WriteFile(fakeAZ, []byte("#!/bin/sh\necho '11111111-2222-3333-4444-555555555555'\n"), 0o755); err != nil { + t.Fatalf("write fake az: %v", err) + } + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + got := defaultSubscriptionID() + if got != "11111111-2222-3333-4444-555555555555" { + t.Fatalf("unexpected subscription id %q", got) + } +} + +func TestAzureTenantIDUsesAZCLI(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping shell-script fake az on Windows") + } + + dir := t.TempDir() + fakeAZ := filepath.Join(dir, "az") + if err := os.WriteFile(fakeAZ, []byte("#!/bin/sh\necho 'tenant-from-az'\n"), 0o755); err != nil { + t.Fatalf("write fake az: %v", err) + } + t.Setenv("PATH", dir+string(os.PathListSeparator)+os.Getenv("PATH")) + + if got := AzureTenantID(); got != "tenant-from-az" { + t.Fatalf("unexpected tenant id %q", got) + } +} diff --git a/plugin/pkg/util/k8s/k8s.go b/plugin/pkg/util/k8s/k8s.go index 1069925..981271d 100644 --- a/plugin/pkg/util/k8s/k8s.go +++ b/plugin/pkg/util/k8s/k8s.go @@ -2,6 +2,9 @@ package k8s import ( "context" + "errors" + "fmt" + "net/url" "os" "path/filepath" @@ -119,3 +122,54 @@ func MergeKubeconfigInto(ctx context.Context, credentials azcore.TokenCredential return os.WriteFile(path, content, 0600) } + +// APIServerFromKubeconfigFile returns the API server hostname and port from +// the kubeconfig file at path. If contextName is non-empty it is used to +// select the context; otherwise the file's current-context is used. +func APIServerFromKubeconfigFile(path, contextName string) (host, port string, err error) { + kcfg, err := clientcmd.LoadFromFile(path) + if err != nil { + return "", "", fmt.Errorf("loading kubeconfig for API server: %w", err) + } + + ctxName := contextName + if ctxName == "" { + ctxName = kcfg.CurrentContext + } + if ctxName == "" { + return "", "", errors.New("kubeconfig missing current context") + } + + ctxCfg, ok := kcfg.Contexts[ctxName] + if !ok || ctxCfg == nil { + return "", "", fmt.Errorf("kubeconfig missing context %q", ctxName) + } + + clusterCfg, ok := kcfg.Clusters[ctxCfg.Cluster] + if !ok || clusterCfg == nil { + return "", "", fmt.Errorf("kubeconfig missing cluster %q", ctxCfg.Cluster) + } + + u, err := url.Parse(clusterCfg.Server) + if err != nil { + return "", "", fmt.Errorf("parsing API server URL %q: %w", clusterCfg.Server, err) + } + + hostname := u.Hostname() + p := u.Port() + if hostname == "" { + return "", "", fmt.Errorf("API server URL missing hostname: %q", clusterCfg.Server) + } + if p == "" { + switch u.Scheme { + case "https": + p = "443" + case "http": + p = "80" + default: + return "", "", fmt.Errorf("API server URL missing port and unsupported scheme %q", u.Scheme) + } + } + + return hostname, p, nil +}