Respect kubeconfig context namespace
Signed-off-by: Jiri Tyr <jiri.tyr@gmail.com>
This commit is contained in:
@@ -100,6 +100,16 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
||||
# Uninstall Flux and delete CRDs
|
||||
flux uninstall`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
// If opted in via --ns-follows-kube-context flag or
|
||||
// FLUX_NS_FOLLOWS_KUBE_CONTEXT env var, and --namespace was not
|
||||
// explicitly set, respect the namespace from the kubeconfig context.
|
||||
if !cmd.Flags().Changed("namespace") &&
|
||||
(rootArgs.nsFollowsKubeContext || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "") {
|
||||
if ctxNs := getKubeconfigContextNamespace(kubeconfigArgs); ctxNs != "" {
|
||||
*kubeconfigArgs.Namespace = ctxNs
|
||||
}
|
||||
}
|
||||
|
||||
ns, err := cmd.Flags().GetString("namespace")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting namespace: %w", err)
|
||||
@@ -116,10 +126,11 @@ Command line utility for assembling Kubernetes CD pipelines the GitOps way.`,
|
||||
var logger = stderrLogger{stderr: os.Stderr}
|
||||
|
||||
type rootFlags struct {
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
pollInterval time.Duration
|
||||
defaults install.Options
|
||||
timeout time.Duration
|
||||
verbose bool
|
||||
pollInterval time.Duration
|
||||
nsFollowsKubeContext bool
|
||||
defaults install.Options
|
||||
}
|
||||
|
||||
// RequestError is a custom error type that wraps an error returned by the flux api.
|
||||
@@ -139,6 +150,8 @@ var kubeclientOptions = new(runclient.Options)
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().DurationVar(&rootArgs.timeout, "timeout", 5*time.Minute, "timeout for this operation")
|
||||
rootCmd.PersistentFlags().BoolVar(&rootArgs.verbose, "verbose", false, "print generated objects")
|
||||
rootCmd.PersistentFlags().BoolVar(&rootArgs.nsFollowsKubeContext, "ns-follows-kube-context", false,
|
||||
"use the namespace from the kubeconfig context instead of the default flux-system namespace, can also be set via FLUX_NS_FOLLOWS_KUBE_CONTEXT env var")
|
||||
|
||||
configureDefaultNamespace()
|
||||
kubeconfigArgs.APIServer = nil // prevent AddFlags from configuring --server flag
|
||||
@@ -205,6 +218,26 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// getKubeconfigContextNamespace returns the namespace from the current
|
||||
// kubeconfig context, or an empty string if it cannot be determined.
|
||||
func getKubeconfigContextNamespace(cf *genericclioptions.ConfigFlags) string {
|
||||
rawConfig, err := cf.ToRawKubeConfigLoader().RawConfig()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
currentContext := rawConfig.CurrentContext
|
||||
if cf.Context != nil && *cf.Context != "" {
|
||||
currentContext = *cf.Context
|
||||
}
|
||||
|
||||
if ctx, ok := rawConfig.Contexts[currentContext]; ok {
|
||||
return ctx.Namespace
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func configureDefaultNamespace() {
|
||||
*kubeconfigArgs.Namespace = rootArgs.defaults.Namespace
|
||||
fromEnv := os.Getenv("FLUX_SYSTEM_NAMESPACE")
|
||||
|
||||
221
cmd/flux/main_context_ns_test.go
Normal file
221
cmd/flux/main_context_ns_test.go
Normal file
@@ -0,0 +1,221 @@
|
||||
/*
|
||||
Copyright 2026 The Flux authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/cli-runtime/pkg/genericclioptions"
|
||||
)
|
||||
|
||||
func TestGetKubeconfigContextNamespace(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
kubeconfig string
|
||||
context string
|
||||
expectedResult string
|
||||
}{
|
||||
{
|
||||
name: "returns namespace from current context",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: custom-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
expectedResult: "custom-ns",
|
||||
},
|
||||
{
|
||||
name: "returns empty when context has no namespace",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
expectedResult: "",
|
||||
},
|
||||
{
|
||||
name: "returns namespace from context specified via --context flag",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: default-context
|
||||
contexts:
|
||||
- name: default-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: default-ns
|
||||
- name: other-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: other-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`,
|
||||
context: "other-context",
|
||||
expectedResult: "other-ns",
|
||||
},
|
||||
{
|
||||
name: "returns empty when context does not exist",
|
||||
kubeconfig: `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: non-existent
|
||||
contexts: []
|
||||
clusters: []
|
||||
`,
|
||||
expectedResult: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Write temporary kubeconfig.
|
||||
tmpDir := t.TempDir()
|
||||
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||
g.Expect(os.WriteFile(kcPath, []byte(tt.kubeconfig), 0o600)).To(Succeed())
|
||||
|
||||
// Use a local ConfigFlags instance to avoid polluting the
|
||||
// package-global kubeconfigArgs (which caches a clientConfig
|
||||
// internally and would leak state across tests).
|
||||
cf := genericclioptions.NewConfigFlags(false)
|
||||
cf.KubeConfig = &kcPath
|
||||
cf.Context = &tt.context
|
||||
|
||||
got := getKubeconfigContextNamespace(cf)
|
||||
g.Expect(got).To(Equal(tt.expectedResult))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextNamespaceOptIn(t *testing.T) {
|
||||
kubeconfig := `apiVersion: v1
|
||||
kind: Config
|
||||
current-context: my-context
|
||||
contexts:
|
||||
- name: my-context
|
||||
context:
|
||||
cluster: my-cluster
|
||||
namespace: context-ns
|
||||
clusters:
|
||||
- name: my-cluster
|
||||
cluster:
|
||||
server: https://localhost:6443
|
||||
`
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
nsFollowsFlag bool
|
||||
nsFollowsEnv string
|
||||
envNamespace string
|
||||
flagNamespace string
|
||||
expectedNamespace string
|
||||
}{
|
||||
{
|
||||
name: "ignores context namespace when not opted in",
|
||||
expectedNamespace: rootArgs.defaults.Namespace,
|
||||
},
|
||||
{
|
||||
name: "uses context namespace when opted in via flag",
|
||||
nsFollowsFlag: true,
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "uses context namespace when opted in via env var",
|
||||
nsFollowsEnv: "1",
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "context namespace takes precedence over FLUX_SYSTEM_NAMESPACE when opted in",
|
||||
nsFollowsFlag: true,
|
||||
envNamespace: "env-ns",
|
||||
expectedNamespace: "context-ns",
|
||||
},
|
||||
{
|
||||
name: "FLUX_SYSTEM_NAMESPACE used when not opted in",
|
||||
envNamespace: "env-ns",
|
||||
expectedNamespace: "env-ns",
|
||||
},
|
||||
{
|
||||
name: "--namespace flag takes precedence over context namespace",
|
||||
nsFollowsFlag: true,
|
||||
flagNamespace: "flag-ns",
|
||||
expectedNamespace: "flag-ns",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
g := NewWithT(t)
|
||||
|
||||
// Write temporary kubeconfig.
|
||||
tmpDir := t.TempDir()
|
||||
kcPath := filepath.Join(tmpDir, "kubeconfig")
|
||||
g.Expect(os.WriteFile(kcPath, []byte(kubeconfig), 0o600)).To(Succeed())
|
||||
|
||||
// Use a local ConfigFlags instance to avoid polluting the
|
||||
// package-global kubeconfigArgs.
|
||||
cf := genericclioptions.NewConfigFlags(false)
|
||||
cf.KubeConfig = &kcPath
|
||||
emptyCtx := ""
|
||||
cf.Context = &emptyCtx
|
||||
|
||||
// Mirror configureDefaultNamespace behavior on the local instance.
|
||||
defaultNs := rootArgs.defaults.Namespace
|
||||
cf.Namespace = &defaultNs
|
||||
|
||||
if tt.envNamespace != "" {
|
||||
t.Setenv("FLUX_SYSTEM_NAMESPACE", tt.envNamespace)
|
||||
envNs := tt.envNamespace
|
||||
cf.Namespace = &envNs
|
||||
}
|
||||
if tt.nsFollowsEnv != "" {
|
||||
t.Setenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT", tt.nsFollowsEnv)
|
||||
}
|
||||
|
||||
// Simulate PersistentPreRunE behavior.
|
||||
if tt.flagNamespace != "" {
|
||||
*cf.Namespace = tt.flagNamespace
|
||||
} else if tt.nsFollowsFlag || os.Getenv("FLUX_NS_FOLLOWS_KUBE_CONTEXT") != "" {
|
||||
if ctxNs := getKubeconfigContextNamespace(cf); ctxNs != "" {
|
||||
*cf.Namespace = ctxNs
|
||||
}
|
||||
}
|
||||
|
||||
g.Expect(*cf.Namespace).To(Equal(tt.expectedNamespace))
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user