github.com/grafana/tanka@v0.26.1-0.20240506093700-c22cfc35c21a/pkg/kubernetes/client/context.go (about) 1 package client 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "errors" 7 "fmt" 8 "os" 9 "regexp" 10 "strings" 11 12 "github.com/stretchr/objx" 13 funk "github.com/thoas/go-funk" 14 ) 15 16 // findContextFromEndpoint returns a valid context from $KUBECONFIG that uses the given 17 // apiServer endpoint. 18 func findContextFromEndpoint(endpoint string) (Config, error) { 19 cluster, context, err := ContextFromIP(endpoint) 20 if err != nil { 21 return Config{}, err 22 } 23 24 return Config{ 25 Context: *context, 26 Cluster: *cluster, 27 }, nil 28 } 29 30 // findContextFromNames will try to match a context name from names 31 func findContextFromNames(names []string) (Config, error) { 32 for _, name := range names { 33 cluster, context, err := ContextFromName(name) 34 35 if _, ok := err.(ErrorNoContext); ok { 36 continue 37 } else if err != nil { 38 return Config{}, err 39 } 40 return Config{ 41 Context: *context, 42 Cluster: *cluster, 43 }, nil 44 } 45 return Config{}, ErrorNoContext(fmt.Sprintf("%v", names)) 46 } 47 48 func ContextFromName(contextName string) (*Cluster, *Context, error) { 49 cfg, err := Kubeconfig() 50 if err != nil { 51 return nil, nil, err 52 } 53 54 // find the context by name 55 var context Context 56 contexts, err := tryMSISlice(cfg.Get("contexts"), "contexts") 57 if err != nil { 58 return nil, nil, err 59 } 60 61 err = find(contexts, "name", fmt.Sprintf("^%s$", contextName), &context) 62 if err == ErrorNoMatch { 63 return nil, nil, ErrorNoContext(contextName) 64 } else if err != nil { 65 return nil, nil, err 66 } 67 var cluster Cluster 68 clusters, err := tryMSISlice(cfg.Get("clusters"), "clusters") 69 if err != nil { 70 return nil, nil, err 71 } 72 73 err = find(clusters, "name", fmt.Sprintf("^%s$", context.Context.Cluster), &cluster) 74 if err == ErrorNoMatch { 75 return nil, nil, ErrorNoCluster(contextName) 76 } else if err != nil { 77 return nil, nil, err 78 } 79 80 return &cluster, &context, nil 81 } 82 83 // Kubeconfig returns the merged $KUBECONFIG of the host 84 func Kubeconfig() (objx.Map, error) { 85 cmd := kubectlCmd("config", "view", "-o", "json") 86 cfgJSON := bytes.Buffer{} 87 cmd.Stdout = &cfgJSON 88 cmd.Stderr = os.Stderr 89 90 if err := cmd.Run(); err != nil { 91 return nil, err 92 } 93 94 return objx.FromJSON(cfgJSON.String()) 95 } 96 97 // Contexts returns a list of context names 98 func Contexts() ([]string, error) { 99 cmd := kubectlCmd("config", "get-contexts", "-o=name") 100 buf := bytes.Buffer{} 101 cmd.Stdout = &buf 102 cmd.Stderr = os.Stderr 103 if err := cmd.Run(); err != nil { 104 return nil, err 105 } 106 107 return strings.Split(buf.String(), "\n"), nil 108 } 109 110 // ContextFromIP searches the $KUBECONFIG for a context using a cluster that matches the apiServer 111 func ContextFromIP(apiServer string) (*Cluster, *Context, error) { 112 cfg, err := Kubeconfig() 113 if err != nil { 114 return nil, nil, err 115 } 116 117 // find the correct cluster 118 var cluster Cluster 119 clusters, err := tryMSISlice(cfg.Get("clusters"), "clusters") 120 if err != nil { 121 return nil, nil, err 122 } 123 124 err = find(clusters, "cluster.server", apiServer, &cluster) 125 if err == ErrorNoMatch { 126 return nil, nil, ErrorNoCluster(apiServer) 127 } else if err != nil { 128 return nil, nil, err 129 } 130 131 // find a context that uses the cluster 132 var context Context 133 contexts, err := tryMSISlice(cfg.Get("contexts"), "contexts") 134 if err != nil { 135 return nil, nil, err 136 } 137 138 // find the context that uses the cluster, it should be an exact match 139 err = find(contexts, "context.cluster", fmt.Sprintf("^%s$", cluster.Name), &context) 140 if err == ErrorNoMatch { 141 return nil, nil, ErrorNoContext(cluster.Name) 142 } else if err != nil { 143 return nil, nil, err 144 } 145 146 return &cluster, &context, nil 147 } 148 149 // IPFromContext parses $KUBECONFIG, finds the cluster with the given name and 150 // returns the cluster's endpoint 151 func IPFromContext(name string) (ip string, err error) { 152 cfg, err := Kubeconfig() 153 if err != nil { 154 return "", err 155 } 156 157 // find a context with the given name 158 var context Context 159 contexts, err := tryMSISlice(cfg.Get("contexts"), "contexts") 160 if err != nil { 161 return "", err 162 } 163 164 err = find(contexts, "name", fmt.Sprintf("^%s$", name), &context) 165 if err == ErrorNoMatch { 166 return "", ErrorNoContext(name) 167 } else if err != nil { 168 return "", err 169 } 170 171 // find the cluster of the context 172 var cluster Cluster 173 clusters, err := tryMSISlice(cfg.Get("clusters"), "clusters") 174 if err != nil { 175 return "", err 176 } 177 178 clusterName := context.Context.Cluster 179 err = find(clusters, "name", fmt.Sprintf("^%s$", clusterName), &cluster) 180 if err == ErrorNoMatch { 181 return "", fmt.Errorf("no cluster named `%s` as required by context `%s` was found. Please check your $KUBECONFIG", clusterName, name) 182 } else if err != nil { 183 return "", err 184 } 185 186 return cluster.Cluster.Server, nil 187 } 188 189 func tryMSISlice(v *objx.Value, what string) ([]map[string]interface{}, error) { 190 if s := v.MSISlice(); s != nil { 191 return s, nil 192 } 193 194 data, ok := v.Data().([]map[string]interface{}) 195 if !ok { 196 return nil, fmt.Errorf("expected %s to be of type `[]map[string]interface{}`, but got `%T` instead", what, v.Data()) 197 } 198 return data, nil 199 } 200 201 // ErrorNoMatch occurs when no item matched had the expected value 202 var ErrorNoMatch = errors.New("no matches found") 203 204 // find attempts to find an object in list whose prop equals expected. 205 // If found, the value is unmarshalled to ptr, otherwise errNotFound is returned. 206 func find(list []map[string]interface{}, prop string, expected string, ptr interface{}) error { 207 var findErr error 208 i := funk.Find(list, func(x map[string]interface{}) bool { 209 if findErr != nil { 210 return false 211 } 212 213 got := objx.New(x).Get(prop).Data() 214 str, ok := got.(string) 215 if !ok { 216 findErr = fmt.Errorf("testing whether `%s` is `%s`: unable to parse `%v` as string", prop, expected, got) 217 return false 218 } 219 return regexp.MustCompile(expected).MatchString(str) 220 }) 221 if findErr != nil { 222 return findErr 223 } 224 225 if i == nil { 226 return ErrorNoMatch 227 } 228 229 o := objx.New(i).MustJSON() 230 return json.Unmarshal([]byte(o), ptr) 231 }