From 9c74400a9a5ed1a80e3b48217754625d6308ace5 Mon Sep 17 00:00:00 2001 From: Qi Ke Date: Fri, 13 Mar 2026 14:13:38 -0400 Subject: [PATCH 1/5] fix node bootstrap script for held kube packages --- cli/internal/config/nodebootstrap/assets/script.sh.tmpl | 2 +- cli/internal/config/nodebootstrap/script_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cli/internal/config/nodebootstrap/assets/script.sh.tmpl b/cli/internal/config/nodebootstrap/assets/script.sh.tmpl index e4eeea9..b75b657 100644 --- a/cli/internal/config/nodebootstrap/assets/script.sh.tmpl +++ b/cli/internal/config/nodebootstrap/assets/script.sh.tmpl @@ -25,7 +25,7 @@ apt-get update -y apt-get upgrade -y {{- end }} {{- if .Packages }} -apt-get install -y {{ join .Packages " " }} +apt-get install -y --allow-change-held-packages {{ join .Packages " " }} {{- end }} {{- end }} {{- if .FQDN }} diff --git a/cli/internal/config/nodebootstrap/script_test.go b/cli/internal/config/nodebootstrap/script_test.go index 3166125..22743c4 100644 --- a/cli/internal/config/nodebootstrap/script_test.go +++ b/cli/internal/config/nodebootstrap/script_test.go @@ -36,7 +36,7 @@ func Test_marshalScript_basic(t *testing.T) { "#!/bin/bash", "set -euo pipefail", "apt-get update -y", - "apt-get install -y curl", + "apt-get install -y --allow-change-held-packages curl", "mkdir -p '/tmp'", `cat <<'EOF' > '/tmp/config.json'`, `{"key":"value"}`, From 9fe58b28f381931be726079e5a23fc6036818833 Mon Sep 17 00:00:00 2001 From: Qi Ke Date: Sat, 14 Mar 2026 13:16:33 -0400 Subject: [PATCH 2/5] fix Azure profile-aware AKS Flex bootstrap --- cli/internal/aks/deploy/cilium.go | 70 ++++++++++++++++++- cli/internal/aks/deploy/cilium_test.go | 42 +++++++++++ cli/internal/config/configcmd/defaults.go | 42 ++++++++++- .../config/configcmd/defaults_test.go | 24 +++++++ plugin/pkg/util/config/config.go | 7 +- plugin/pkg/util/config/config_test.go | 27 +++++++ 6 files changed, 207 insertions(+), 5 deletions(-) create mode 100644 cli/internal/aks/deploy/cilium_test.go create mode 100644 cli/internal/config/configcmd/defaults_test.go create mode 100644 plugin/pkg/util/config/config_test.go diff --git a/cli/internal/aks/deploy/cilium.go b/cli/internal/aks/deploy/cilium.go index 46ca558..38fb546 100644 --- a/cli/internal/aks/deploy/cilium.go +++ b/cli/internal/aks/deploy/cilium.go @@ -3,10 +3,14 @@ package deploy import ( "context" "errors" + "fmt" + "log" + "net/url" "os" "os/exec" "github.com/Azure/aks-flex/plugin/pkg/util/config" + "k8s.io/client-go/tools/clientcmd" ) var ciliumInstallInstruction = errors.New( @@ -28,18 +32,78 @@ func deployCilium( kubeconfigFile string, cfg *config.Config, ) error { + k8sServiceHost, k8sServicePort, err := kubeconfigAPIServer(kubeconfigFile) + if err != nil { + return err + } + cmd := exec.CommandContext( ctx, "cilium", "install", - "--set", "azure.resourceGroup="+cfg.ResourceGroupName, + "--kubeconfig", kubeconfigFile, + "--context", cfg.ClusterName+"-admin", + "--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 + "PATH="+os.Getenv("PATH"), ) + 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, cfg.ClusterName+"-admin", cfg.ClusterName, k8sServiceHost, k8sServicePort) return cmd.Run() } + +func kubeconfigAPIServer(kubeconfigFile string) (string, string, error) { + kcfg, err := clientcmd.LoadFromFile(kubeconfigFile) + if err != nil { + return "", "", fmt.Errorf("loading kubeconfig for cilium install: %w", err) + } + + 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() + port := u.Port() + if hostname == "" { + return "", "", fmt.Errorf("API server URL missing hostname: %q", clusterCfg.Server) + } + if port == "" { + switch u.Scheme { + case "https": + port = "443" + case "http": + port = "80" + default: + return "", "", fmt.Errorf("API server URL missing port and unsupported scheme %q", u.Scheme) + } + } + + return hostname, port, nil +} diff --git a/cli/internal/aks/deploy/cilium_test.go b/cli/internal/aks/deploy/cilium_test.go new file mode 100644 index 0000000..c5ec79e --- /dev/null +++ b/cli/internal/aks/deploy/cilium_test.go @@ -0,0 +1,42 @@ +package deploy + +import ( + "os" + "path/filepath" + "testing" +) + +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 := kubeconfigAPIServer(kubeconfig) + if err != nil { + t.Fatalf("kubeconfigAPIServer 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..a3f310b 100644 --- a/cli/internal/config/configcmd/defaults.go +++ b/cli/internal/config/configcmd/defaults.go @@ -2,8 +2,10 @@ package configcmd import ( "context" + "encoding/json" "fmt" "os" + "path/filepath" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -42,7 +44,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 := azureConfigTenantID(); 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.") @@ -64,3 +70,37 @@ func DefaultKubeadmConfig(ctx context.Context) *kubeadm.Config { } return cfg } + +func azureConfigTenantID() string { + azureConfigDir := os.Getenv("AZURE_CONFIG_DIR") + if azureConfigDir == "" { + azureConfigDir = filepath.Join(os.Getenv("HOME"), ".azure") + } + + b, err := os.ReadFile(filepath.Join(azureConfigDir, "azureProfile.json")) + if err != nil { + return "" + } + + var profile struct { + Subscriptions []struct { + IsDefault bool `json:"isDefault"` + TenantID string `json:"tenantId"` + } `json:"subscriptions"` + } + if err := json.Unmarshal(b, &profile); err != nil { + return "" + } + + for _, sub := range profile.Subscriptions { + if sub.IsDefault && sub.TenantID != "" { + return sub.TenantID + } + } + + if len(profile.Subscriptions) == 1 { + return profile.Subscriptions[0].TenantID + } + + return "" +} diff --git a/cli/internal/config/configcmd/defaults_test.go b/cli/internal/config/configcmd/defaults_test.go new file mode 100644 index 0000000..1fd7af8 --- /dev/null +++ b/cli/internal/config/configcmd/defaults_test.go @@ -0,0 +1,24 @@ +package configcmd + +import ( + "os" + "path/filepath" + "testing" +) + +func TestAzureConfigTenantIDUsesAzureConfigDirProfile(t *testing.T) { + home := t.TempDir() + t.Setenv("HOME", home) + azureConfigDir := filepath.Join(t.TempDir(), "azure-profile") + if err := os.MkdirAll(azureConfigDir, 0o755); err != nil { + t.Fatalf("mkdir azure config dir: %v", err) + } + if err := os.WriteFile(filepath.Join(azureConfigDir, "azureProfile.json"), []byte(`{"subscriptions":[{"id":"sub","isDefault":true,"tenantId":"tenant-123"}]}`), 0o600); err != nil { + t.Fatalf("write azureProfile.json: %v", err) + } + t.Setenv("AZURE_CONFIG_DIR", azureConfigDir) + + if got := azureConfigTenantID(); got != "tenant-123" { + t.Fatalf("unexpected tenant id %q", got) + } +} diff --git a/plugin/pkg/util/config/config.go b/plugin/pkg/util/config/config.go index 7da7d0b..23c5bab 100644 --- a/plugin/pkg/util/config/config.go +++ b/plugin/pkg/util/config/config.go @@ -107,7 +107,12 @@ func defaultSubscriptionID() string { return subscriptionID } - b, err := os.ReadFile(filepath.Join(os.Getenv("HOME"), ".azure/clouds.config")) + azureConfigDir := os.Getenv("AZURE_CONFIG_DIR") + if azureConfigDir == "" { + azureConfigDir = filepath.Join(os.Getenv("HOME"), ".azure") + } + + b, err := os.ReadFile(filepath.Join(azureConfigDir, "clouds.config")) if err != nil { return "" } diff --git a/plugin/pkg/util/config/config_test.go b/plugin/pkg/util/config/config_test.go new file mode 100644 index 0000000..1710aa7 --- /dev/null +++ b/plugin/pkg/util/config/config_test.go @@ -0,0 +1,27 @@ +package config + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDefaultSubscriptionIDHonorsAzureConfigDir(t *testing.T) { + t.Setenv("AZURE_SUBSCRIPTION_ID", "") + home := t.TempDir() + t.Setenv("HOME", home) + + azureConfigDir := filepath.Join(t.TempDir(), "azure-custom") + if err := os.MkdirAll(azureConfigDir, 0o755); err != nil { + t.Fatalf("mkdir azureConfigDir: %v", err) + } + if err := os.WriteFile(filepath.Join(azureConfigDir, "clouds.config"), []byte("[AzureCloud]\nsubscription = 11111111-2222-3333-4444-555555555555\n"), 0o600); err != nil { + t.Fatalf("write clouds.config: %v", err) + } + t.Setenv("AZURE_CONFIG_DIR", azureConfigDir) + + got := defaultSubscriptionID() + if got != "11111111-2222-3333-4444-555555555555" { + t.Fatalf("unexpected subscription id %q", got) + } +} From 3cac673c81fa8c8fba596c97b334d35afa0c14c3 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:43:41 -0700 Subject: [PATCH 3/5] Refactor: use `az account show` for tenant ID and subscription ID; move kubeconfig/k8s helpers to shared packages (#55) * Initial plan * Address review feedback: use az CLI for tenant ID, move utility functions to shared packages Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * Use az CLI for subscription ID; fix indentation in APIServerFromKubeconfigFile Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * Add contextName parameter to APIServerFromKubeconfigFile; pass context in cilium deploy Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> * Extract clusterContext variable in deployCilium; reuse across API server lookup and cilium install flags Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> --- cli/internal/aks/deploy/cilium.go | 56 ++----------------- cli/internal/aks/deploy/cilium_test.go | 6 +- cli/internal/config/configcmd/defaults.go | 38 +------------ .../config/configcmd/defaults_test.go | 23 -------- plugin/pkg/util/config/config.go | 29 +++++----- plugin/pkg/util/config/config_test.go | 37 ++++++++---- plugin/pkg/util/k8s/k8s.go | 54 ++++++++++++++++++ 7 files changed, 104 insertions(+), 139 deletions(-) diff --git a/cli/internal/aks/deploy/cilium.go b/cli/internal/aks/deploy/cilium.go index 38fb546..a3131f6 100644 --- a/cli/internal/aks/deploy/cilium.go +++ b/cli/internal/aks/deploy/cilium.go @@ -3,14 +3,12 @@ package deploy import ( "context" "errors" - "fmt" "log" - "net/url" "os" "os/exec" "github.com/Azure/aks-flex/plugin/pkg/util/config" - "k8s.io/client-go/tools/clientcmd" + "github.com/Azure/aks-flex/plugin/pkg/util/k8s" ) var ciliumInstallInstruction = errors.New( @@ -32,7 +30,8 @@ func deployCilium( kubeconfigFile string, cfg *config.Config, ) error { - k8sServiceHost, k8sServicePort, err := kubeconfigAPIServer(kubeconfigFile) + clusterContext := cfg.ClusterName + "-admin" + k8sServiceHost, k8sServicePort, err := k8s.APIServerFromKubeconfigFile(kubeconfigFile, clusterContext) if err != nil { return err } @@ -41,7 +40,7 @@ func deployCilium( ctx, "cilium", "install", "--kubeconfig", kubeconfigFile, - "--context", cfg.ClusterName+"-admin", + "--context", clusterContext, "--namespace", "kube-system", "--datapath-mode", "aks-byocni", "--helm-set", "aksbyocni.enabled=true", @@ -58,52 +57,7 @@ func deployCilium( "KUBECONFIG="+kubeconfigFile, "PATH="+os.Getenv("PATH"), ) - 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, cfg.ClusterName+"-admin", cfg.ClusterName, k8sServiceHost, k8sServicePort) + 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() } - -func kubeconfigAPIServer(kubeconfigFile string) (string, string, error) { - kcfg, err := clientcmd.LoadFromFile(kubeconfigFile) - if err != nil { - return "", "", fmt.Errorf("loading kubeconfig for cilium install: %w", err) - } - - 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() - port := u.Port() - if hostname == "" { - return "", "", fmt.Errorf("API server URL missing hostname: %q", clusterCfg.Server) - } - if port == "" { - switch u.Scheme { - case "https": - port = "443" - case "http": - port = "80" - default: - return "", "", fmt.Errorf("API server URL missing port and unsupported scheme %q", u.Scheme) - } - } - - return hostname, port, nil -} diff --git a/cli/internal/aks/deploy/cilium_test.go b/cli/internal/aks/deploy/cilium_test.go index c5ec79e..c8706fb 100644 --- a/cli/internal/aks/deploy/cilium_test.go +++ b/cli/internal/aks/deploy/cilium_test.go @@ -4,6 +4,8 @@ import ( "os" "path/filepath" "testing" + + k8sutil "github.com/Azure/aks-flex/plugin/pkg/util/k8s" ) func TestKubeconfigAPIServer(t *testing.T) { @@ -29,9 +31,9 @@ users: t.Fatalf("write kubeconfig: %v", err) } - host, port, err := kubeconfigAPIServer(kubeconfig) + host, port, err := k8sutil.APIServerFromKubeconfigFile(kubeconfig, "") if err != nil { - t.Fatalf("kubeconfigAPIServer returned error: %v", err) + t.Fatalf("APIServerFromKubeconfigFile returned error: %v", err) } if host != "example.hcp.eastus2.azmk8s.io" { t.Fatalf("unexpected host %q", host) diff --git a/cli/internal/config/configcmd/defaults.go b/cli/internal/config/configcmd/defaults.go index a3f310b..d366141 100644 --- a/cli/internal/config/configcmd/defaults.go +++ b/cli/internal/config/configcmd/defaults.go @@ -2,10 +2,8 @@ package configcmd import ( "context" - "encoding/json" "fmt" "os" - "path/filepath" "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" "github.com/Azure/azure-sdk-for-go/sdk/azidentity" @@ -45,7 +43,7 @@ func OrPlaceholder(val string) string { // to placeholder values that the user must replace manually. func DefaultKubeadmConfig(ctx context.Context) *kubeadm.Config { credOptions := &azidentity.AzureCLICredentialOptions{} - if tenantID := azureConfigTenantID(); tenantID != "" { + if tenantID := config.AzureTenantID(); tenantID != "" { credOptions.TenantID = tenantID } credentials, err := azidentity.NewAzureCLICredential(credOptions) @@ -70,37 +68,3 @@ func DefaultKubeadmConfig(ctx context.Context) *kubeadm.Config { } return cfg } - -func azureConfigTenantID() string { - azureConfigDir := os.Getenv("AZURE_CONFIG_DIR") - if azureConfigDir == "" { - azureConfigDir = filepath.Join(os.Getenv("HOME"), ".azure") - } - - b, err := os.ReadFile(filepath.Join(azureConfigDir, "azureProfile.json")) - if err != nil { - return "" - } - - var profile struct { - Subscriptions []struct { - IsDefault bool `json:"isDefault"` - TenantID string `json:"tenantId"` - } `json:"subscriptions"` - } - if err := json.Unmarshal(b, &profile); err != nil { - return "" - } - - for _, sub := range profile.Subscriptions { - if sub.IsDefault && sub.TenantID != "" { - return sub.TenantID - } - } - - if len(profile.Subscriptions) == 1 { - return profile.Subscriptions[0].TenantID - } - - return "" -} diff --git a/cli/internal/config/configcmd/defaults_test.go b/cli/internal/config/configcmd/defaults_test.go index 1fd7af8..1ab3551 100644 --- a/cli/internal/config/configcmd/defaults_test.go +++ b/cli/internal/config/configcmd/defaults_test.go @@ -1,24 +1 @@ package configcmd - -import ( - "os" - "path/filepath" - "testing" -) - -func TestAzureConfigTenantIDUsesAzureConfigDirProfile(t *testing.T) { - home := t.TempDir() - t.Setenv("HOME", home) - azureConfigDir := filepath.Join(t.TempDir(), "azure-profile") - if err := os.MkdirAll(azureConfigDir, 0o755); err != nil { - t.Fatalf("mkdir azure config dir: %v", err) - } - if err := os.WriteFile(filepath.Join(azureConfigDir, "azureProfile.json"), []byte(`{"subscriptions":[{"id":"sub","isDefault":true,"tenantId":"tenant-123"}]}`), 0o600); err != nil { - t.Fatalf("write azureProfile.json: %v", err) - } - t.Setenv("AZURE_CONFIG_DIR", azureConfigDir) - - if got := azureConfigTenantID(); got != "tenant-123" { - t.Fatalf("unexpected tenant id %q", got) - } -} diff --git a/plugin/pkg/util/config/config.go b/plugin/pkg/util/config/config.go index 23c5bab..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,27 +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 } - azureConfigDir := os.Getenv("AZURE_CONFIG_DIR") - if azureConfigDir == "" { - azureConfigDir = filepath.Join(os.Getenv("HOME"), ".azure") - } - - b, err := os.ReadFile(filepath.Join(azureConfigDir, "clouds.config")) - if err != nil { - return "" - } - - f, err := ini.Load(b) + out, err := exec.Command("az", "account", "show", "--query", "id", "-o", "tsv").Output() 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 index 1710aa7..b4b295a 100644 --- a/plugin/pkg/util/config/config_test.go +++ b/plugin/pkg/util/config/config_test.go @@ -3,25 +3,42 @@ package config import ( "os" "path/filepath" + "runtime" "testing" ) -func TestDefaultSubscriptionIDHonorsAzureConfigDir(t *testing.T) { +func TestDefaultSubscriptionIDUsesAZCLI(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("skipping shell-script fake az on Windows") + } t.Setenv("AZURE_SUBSCRIPTION_ID", "") - home := t.TempDir() - t.Setenv("HOME", home) - azureConfigDir := filepath.Join(t.TempDir(), "azure-custom") - if err := os.MkdirAll(azureConfigDir, 0o755); err != nil { - t.Fatalf("mkdir azureConfigDir: %v", err) - } - if err := os.WriteFile(filepath.Join(azureConfigDir, "clouds.config"), []byte("[AzureCloud]\nsubscription = 11111111-2222-3333-4444-555555555555\n"), 0o600); err != nil { - t.Fatalf("write clouds.config: %v", err) + 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("AZURE_CONFIG_DIR", azureConfigDir) + 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 +} From 15b9fa1d99aead82d550726dbc30da4630895fc5 Mon Sep 17 00:00:00 2001 From: hbc Date: Mon, 16 Mar 2026 18:44:59 -0700 Subject: [PATCH 4/5] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- cli/internal/aks/deploy/cilium.go | 1 - 1 file changed, 1 deletion(-) diff --git a/cli/internal/aks/deploy/cilium.go b/cli/internal/aks/deploy/cilium.go index a3131f6..dc00d3d 100644 --- a/cli/internal/aks/deploy/cilium.go +++ b/cli/internal/aks/deploy/cilium.go @@ -55,7 +55,6 @@ func deployCilium( cmd.Env = append( cmd.Environ(), "KUBECONFIG="+kubeconfigFile, - "PATH="+os.Getenv("PATH"), ) 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) From d3a67ef65bc70798dcda7f22c438bf235974b24e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Mon, 16 Mar 2026 18:51:56 -0700 Subject: [PATCH 5/5] Run go mod tidy across all submodules (#56) * Initial plan * Run go mod tidy across all submodules (cli, karpenter, plugin) Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: bcho <1975118+bcho@users.noreply.github.com> --- cli/go.mod | 1 - cli/go.sum | 3 --- karpenter/go.mod | 1 - karpenter/go.sum | 2 -- plugin/go.mod | 1 - plugin/go.sum | 3 --- 6 files changed, 11 deletions(-) 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/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=