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  }