package kubesource import ( "fmt" "os" "k8s.io/client-go/dynamic" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" ) // ClientOptions say where the cluster is. type ClientOptions struct { // Kubeconfig is a kubeconfig path. Empty means: the in-cluster service // account if there is one, else the standard loading rules ($KUBECONFIG, // then ~/.kube/config). The same binary therefore runs in a pod and on a // workstation with no flag at all. Kubeconfig string // Context selects a kubeconfig context; empty is the file's current one. Context string } // Namespace returns the namespace to watch when none was configured: the // pod's own when running in-cluster, else "" (every namespace the credentials // can list). func Namespace() string { b, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace") if err != nil { return "" } return string(b) } // NewClient builds a dynamic client and reports which source it used. func NewClient(o ClientOptions) (dynamic.Interface, string, error) { cfg, from, err := restConfig(o) if err != nil { return nil, "", err } // The runtime lists and watches a handful of resources; the defaults // (5 QPS) are fine for that, but a hung apiserver must not hang us. cfg.UserAgent = "fancy-dns" c, err := dynamic.NewForConfig(cfg) if err != nil { return nil, "", fmt.Errorf("kubernetes client: %w", err) } return c, from, nil } func restConfig(o ClientOptions) (*rest.Config, string, error) { if o.Kubeconfig == "" && o.Context == "" { if cfg, err := rest.InClusterConfig(); err == nil { return cfg, "in-cluster service account", nil } } rules := clientcmd.NewDefaultClientConfigLoadingRules() if o.Kubeconfig != "" { rules.ExplicitPath = o.Kubeconfig } cfg, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( rules, &clientcmd.ConfigOverrides{CurrentContext: o.Context}).ClientConfig() if err != nil { return nil, "", fmt.Errorf("no usable Kubernetes credentials (in-cluster or kubeconfig): %w", err) } return cfg, "kubeconfig", nil }