github.com/splunk/dan1-qbec@v0.7.3/internal/remote/config.go (about) 1 /* 2 Copyright 2019 Splunk Inc. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package remote has the client implementation for interrogating and updating K8s objects and their metadata. 18 package remote 19 20 import ( 21 "fmt" 22 "path/filepath" 23 "strings" 24 "sync" 25 26 "github.com/pkg/errors" 27 "github.com/spf13/cobra" 28 "github.com/splunk/qbec/internal/sio" 29 "k8s.io/client-go/discovery" 30 "k8s.io/client-go/dynamic" 31 "k8s.io/client-go/rest" 32 "k8s.io/client-go/tools/clientcmd" 33 ) 34 35 // inspired by the config code in ksonnet but implemented differently. 36 37 // ConnectOpts are the connection options required for the config. 38 type ConnectOpts struct { 39 EnvName string // environment name, display purposes only 40 ServerURL string // the server URL to connect to, must be configured in the kubeconfig 41 Namespace string // the default namespace to set for the context 42 Verbosity int // verbosity of client interactions 43 } 44 45 // Config provides clients for specific contexts out of a kubeconfig file, with overrides for auth. 46 type Config struct { 47 loadingRules *clientcmd.ClientConfigLoadingRules 48 overrides *clientcmd.ConfigOverrides 49 l sync.Mutex 50 kubeconfig clientcmd.ClientConfig 51 } 52 53 // NewConfig returns a new configuration, adding flags to the supplied command to set k8s access overrides, prefixed by 54 // the supplied string. 55 func NewConfig(cmd *cobra.Command, prefix string) *Config { 56 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 57 overrides := &clientcmd.ConfigOverrides{} 58 cmd.PersistentFlags().StringVar(&loadingRules.ExplicitPath, prefix+"kubeconfig", "", "Path to a kubeconfig file. Alternative to env var $KUBECONFIG.") 59 clientcmd.BindOverrideFlags(overrides, cmd.PersistentFlags(), clientcmd.ConfigOverrideFlags{ 60 AuthOverrideFlags: clientcmd.RecommendedAuthOverrideFlags(prefix), 61 Timeout: clientcmd.FlagInfo{ 62 LongName: prefix + clientcmd.FlagTimeout, 63 Default: "0", 64 Description: "The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests."}, 65 }) 66 return &Config{ 67 loadingRules: loadingRules, 68 overrides: overrides, 69 } 70 } 71 72 func (c *Config) getRESTConfig(opts ConnectOpts) (*rest.Config, error) { 73 if c.kubeconfig == nil { 74 c.kubeconfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(c.loadingRules, c.overrides) 75 } 76 if err := c.overrideCluster(c.kubeconfig, opts); err != nil { 77 return nil, err 78 } 79 restConfig, err := c.kubeconfig.ClientConfig() 80 if err != nil { 81 return nil, err 82 } 83 return restConfig, nil 84 } 85 86 func (c *Config) overrideCluster(kc clientcmd.ClientConfig, opts ConnectOpts) error { 87 rc, err := kc.RawConfig() 88 if err != nil { 89 return errors.Wrap(err, "raw Config from kubeconfig") 90 } 91 for name, cluster := range rc.Clusters { 92 if cluster.Server == opts.ServerURL { 93 sio.Noticeln("setting cluster to", name) 94 c.overrides.Context.Cluster = name 95 c.overrides.Context.Namespace = opts.Namespace 96 for contextName, ctx := range rc.Contexts { 97 if ctx.Cluster == name { 98 sio.Noticeln("setting context to", contextName) 99 c.overrides.CurrentContext = contextName 100 } 101 } 102 return nil 103 } 104 } 105 return fmt.Errorf("unable to find any cluster with URL %q (for env %s) in the kube config", opts.ServerURL, opts.EnvName) 106 } 107 108 // KubeAttributes is a collection k8s attributes pertaining to an connection. 109 type KubeAttributes struct { 110 ConfigFile string `json:"configFile"` // the kubeconfig file or a list of such file separated by the list path separator 111 Context string `json:"context"` // the context to use, if known 112 Cluster string `json:"cluster"` // the cluster to use, always set 113 Namespace string `json:"namespace"` // the nanespace to use 114 } 115 116 // KubeAttributes returns client attributes for the supplied connection options. 117 func (c *Config) KubeAttributes(opts ConnectOpts) (*KubeAttributes, error) { 118 if c.kubeconfig == nil { 119 c.kubeconfig = clientcmd.NewNonInteractiveDeferredLoadingClientConfig(c.loadingRules, c.overrides) 120 } 121 if err := c.overrideCluster(c.kubeconfig, opts); err != nil { 122 return nil, err 123 } 124 return &KubeAttributes{ 125 ConfigFile: strings.Join(c.loadingRules.Precedence, string(filepath.ListSeparator)), 126 Cluster: c.overrides.Context.Cluster, 127 Context: c.overrides.CurrentContext, 128 Namespace: opts.Namespace, 129 }, nil 130 } 131 132 // Client returns a client that correctly points to the server as specified in the connection options. 133 // For this to work correctly, the kubernetes config that is used *must* have a cluster that has the supplied 134 // server URL as an endpoint, so that correct TLS certs are used for authenticating the server. 135 func (c *Config) Client(opts ConnectOpts) (*Client, error) { 136 c.l.Lock() 137 defer c.l.Unlock() 138 conf, err := c.getRESTConfig(opts) 139 if err != nil { 140 return nil, err 141 } 142 143 disco, err := discovery.NewDiscoveryClientForConfig(conf) 144 if err != nil { 145 return nil, err 146 } 147 148 discoCache := newCachedDiscoveryClient(disco) 149 mapper := discovery.NewDeferredDiscoveryRESTMapper(discoCache, dynamic.VersionInterfaces) 150 pathResolver := dynamic.LegacyAPIPathResolverFunc 151 pool := dynamic.NewClientPool(conf, mapper, pathResolver) 152 return newClient(pool, disco, opts.Namespace, opts.Verbosity) 153 } 154 155 // ContextInfo has information we care about a K8s context 156 type ContextInfo struct { 157 ServerURL string // the server URL defined for the cluster 158 Namespace string // the namespace if set for the context, else "default" 159 } 160 161 // CurrentContextInfo returns information for the current context found in kubeconfig. 162 func CurrentContextInfo() (*ContextInfo, error) { 163 loadingRules := clientcmd.NewDefaultClientConfigLoadingRules() 164 overrides := &clientcmd.ConfigOverrides{} 165 cc := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, overrides) 166 kc, err := cc.RawConfig() 167 if err != nil { 168 return nil, err 169 } 170 if kc.CurrentContext == "" { 171 return nil, fmt.Errorf("no current context set") 172 } 173 var cluster string 174 ns := "default" 175 for name, ctx := range kc.Contexts { 176 if name == kc.CurrentContext { 177 if ctx.Namespace != "" { 178 ns = ctx.Namespace 179 } 180 cluster = ctx.Cluster 181 } 182 } 183 if cluster == "" { 184 return nil, fmt.Errorf("no cluster found for context %s", kc.CurrentContext) 185 } 186 var serverURL string 187 for cname, clusterInfo := range kc.Clusters { 188 if cluster == cname { 189 serverURL = clusterInfo.Server 190 } 191 } 192 if serverURL == "" { 193 return nil, fmt.Errorf("unable to find server URL for cluster %s", cluster) 194 } 195 return &ContextInfo{ 196 ServerURL: serverURL, 197 Namespace: ns, 198 }, nil 199 }