sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/kube/config.go (about)

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     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 kube
    18  
    19  import (
    20  	"errors"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"strings"
    25  
    26  	"github.com/sirupsen/logrus"
    27  	"k8s.io/apimachinery/pkg/util/sets"
    28  	"k8s.io/client-go/rest"
    29  	"k8s.io/client-go/tools/clientcmd"
    30  
    31  	"sigs.k8s.io/prow/pkg/version"
    32  )
    33  
    34  func kubeConfigs(loader clientcmd.ClientConfigLoader) (map[string]rest.Config, string, error) {
    35  	cfg, err := loader.Load()
    36  	if err != nil {
    37  		return nil, "", fmt.Errorf("failed to load: %w", err)
    38  	}
    39  	configs := map[string]rest.Config{}
    40  	for context := range cfg.Contexts {
    41  		contextCfg, err := clientcmd.NewNonInteractiveClientConfig(*cfg, context, &clientcmd.ConfigOverrides{}, loader).ClientConfig()
    42  		if err != nil {
    43  			return nil, "", fmt.Errorf("create %s client: %w", context, err)
    44  		}
    45  		contextCfg.UserAgent = version.UserAgent()
    46  		configs[context] = *contextCfg
    47  		logrus.Infof("Parsed kubeconfig context: %s", context)
    48  	}
    49  	return configs, cfg.CurrentContext, nil
    50  }
    51  
    52  func mergeConfigs(local *rest.Config, foreign map[string]rest.Config, currentContext string) (map[string]rest.Config, error) {
    53  	ret := map[string]rest.Config{}
    54  	for ctx, cfg := range foreign {
    55  		ret[ctx] = cfg
    56  	}
    57  	if local != nil {
    58  		ret[InClusterContext] = *local
    59  	} else if currentContext != "" {
    60  		ret[InClusterContext] = ret[currentContext]
    61  	} else {
    62  		return nil, errors.New("no prow cluster access: in-cluster current kubecfg context required")
    63  	}
    64  	if len(ret) == 0 {
    65  		return nil, errors.New("no client contexts found")
    66  	}
    67  	if _, ok := ret[DefaultClusterAlias]; !ok {
    68  		ret[DefaultClusterAlias] = ret[InClusterContext]
    69  	}
    70  	return ret, nil
    71  }
    72  
    73  // LoadClusterConfigs loads rest.Configs for creation of clients according to the given options.
    74  // Errors are returned if a file/dir is specified in the options and invalid or if no valid contexts are found.
    75  func LoadClusterConfigs(opts *Options) (map[string]rest.Config, error) {
    76  
    77  	logrus.Infof("Loading cluster contexts...")
    78  	// This will work if we are running inside kubernetes
    79  	localCfg, err := rest.InClusterConfig()
    80  	if err != nil {
    81  		logrus.WithError(err).Warn("Could not create in-cluster config (expected when running outside the cluster).")
    82  	} else {
    83  		localCfg.UserAgent = version.UserAgent()
    84  	}
    85  	if localCfg != nil && opts.projectedTokenFile != "" {
    86  		localCfg.BearerToken = ""
    87  		localCfg.BearerTokenFile = opts.projectedTokenFile
    88  		logrus.WithField("tokenfile", opts.projectedTokenFile).Info("Using projected token file")
    89  	}
    90  
    91  	var candidates []string
    92  	if opts.file != "" {
    93  		candidates = append(candidates, opts.file)
    94  	}
    95  	if opts.dir != "" {
    96  		files, err := os.ReadDir(opts.dir)
    97  		if err != nil {
    98  			return nil, fmt.Errorf("kubecfg dir: %w", err)
    99  		}
   100  		for _, file := range files {
   101  			filename := file.Name()
   102  			if file.IsDir() {
   103  				logrus.WithField("dir", filename).Info("Ignored directory")
   104  				continue
   105  			}
   106  			if strings.HasPrefix(filename, "..") {
   107  				logrus.WithField("filename", filename).Info("Ignored file starting with double dots")
   108  				continue
   109  			}
   110  			if !strings.HasSuffix(filename, opts.suffix) {
   111  				logrus.WithField("filename", filename).WithField("suffix", opts.suffix).Info("Ignored file without suffix")
   112  				continue
   113  			}
   114  			candidates = append(candidates, filepath.Join(opts.dir, filename))
   115  		}
   116  	}
   117  
   118  	allKubeCfgs := map[string]rest.Config{}
   119  	var currentContext string
   120  	if len(candidates) == 0 {
   121  		// loading from the defaults, e.g., ${KUBECONFIG}
   122  		if allKubeCfgs, currentContext, err = kubeConfigs(clientcmd.NewDefaultClientConfigLoadingRules()); err != nil {
   123  			logrus.WithError(err).Warn("Cannot load kubecfg")
   124  		}
   125  	} else {
   126  		for _, candidate := range candidates {
   127  			logrus.Infof("Loading kubeconfig from: %q", candidate)
   128  			kubeCfgs, tempCurrentContext, err := kubeConfigs(&clientcmd.ClientConfigLoadingRules{ExplicitPath: candidate})
   129  			if err != nil {
   130  				return nil, fmt.Errorf("fail to load kubecfg from %q: %w", candidate, err)
   131  			}
   132  			if !opts.disabledClusters.Has(tempCurrentContext) {
   133  				currentContext = tempCurrentContext
   134  			}
   135  			for c, k := range kubeCfgs {
   136  				if _, ok := allKubeCfgs[c]; ok {
   137  					return nil, fmt.Errorf("context %s occurred more than once in kubeconfig dir %q", c, opts.dir)
   138  				}
   139  				allKubeCfgs[c] = k
   140  			}
   141  		}
   142  	}
   143  
   144  	for _, disabledCluster := range opts.disabledClusters.UnsortedList() {
   145  		delete(allKubeCfgs, disabledCluster)
   146  		logrus.WithField("disabledCluster", disabledCluster).Info("Removed kubeconfig for disabled cluster")
   147  	}
   148  
   149  	if opts.noInClusterConfig {
   150  		return allKubeCfgs, nil
   151  	}
   152  	return mergeConfigs(localCfg, allKubeCfgs, currentContext)
   153  }
   154  
   155  // Options defines how to load kubeconfigs files
   156  type Options struct {
   157  	file               string
   158  	dir                string
   159  	suffix             string
   160  	projectedTokenFile string
   161  	noInClusterConfig  bool
   162  	disabledClusters   sets.Set[string]
   163  }
   164  
   165  type ConfigOptions func(*Options)
   166  
   167  // ConfigDir configures the directory containing kubeconfig files
   168  func ConfigDir(dir string) ConfigOptions {
   169  	return func(kc *Options) {
   170  		kc.dir = dir
   171  	}
   172  }
   173  
   174  // DisabledClusters configures the set of disabled build cluster names.
   175  // They will be ignored as context names while loading kubeconfig files.
   176  func DisabledClusters(disabledClusters sets.Set[string]) ConfigOptions {
   177  	return func(kc *Options) {
   178  		kc.disabledClusters = disabledClusters
   179  	}
   180  }
   181  
   182  // ConfigSuffix configures the suffix of the file in directory containing kubeconfig files
   183  func ConfigSuffix(suffix string) ConfigOptions {
   184  	return func(kc *Options) {
   185  		kc.suffix = suffix
   186  	}
   187  }
   188  
   189  // ConfigFile configures the path to a kubeconfig file
   190  func ConfigFile(file string) ConfigOptions {
   191  	return func(kc *Options) {
   192  		kc.file = file
   193  	}
   194  }
   195  
   196  // ConfigProjectedTokenFile configures the path to a projectedToken file
   197  func ConfigProjectedTokenFile(projectedTokenFile string) ConfigOptions {
   198  	return func(kc *Options) {
   199  		kc.projectedTokenFile = projectedTokenFile
   200  	}
   201  }
   202  
   203  // NoInClusterConfig indicates that there is no InCluster Config to load
   204  func NoInClusterConfig(noInClusterConfig bool) ConfigOptions {
   205  	return func(kc *Options) {
   206  		kc.noInClusterConfig = noInClusterConfig
   207  	}
   208  }
   209  
   210  // NewConfig builds Options according to the given ConfigOptions
   211  func NewConfig(opts ...ConfigOptions) *Options {
   212  	kc := &Options{}
   213  	for _, opt := range opts {
   214  		opt(kc)
   215  	}
   216  	return kc
   217  }