sigs.k8s.io/cluster-api@v1.7.1/cmd/clusterctl/client/config/reader_viper.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 config
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"net/http"
    24  	"net/url"
    25  	"os"
    26  	"path/filepath"
    27  	"strings"
    28  	"time"
    29  
    30  	"github.com/adrg/xdg"
    31  	"github.com/pkg/errors"
    32  	"github.com/spf13/viper"
    33  
    34  	logf "sigs.k8s.io/cluster-api/cmd/clusterctl/log"
    35  )
    36  
    37  const (
    38  	// ConfigFolder defines the old name of the config folder under $HOME.
    39  	ConfigFolder = ".cluster-api"
    40  	// ConfigFolderXDG defines the name of the config folder under $XDG_CONFIG_HOME.
    41  	ConfigFolderXDG = "cluster-api"
    42  	// ConfigName defines the name of the config file under ConfigFolderXDG.
    43  	ConfigName = "clusterctl"
    44  	// DownloadConfigFile is the config file when fetching the config from a remote location.
    45  	DownloadConfigFile = "clusterctl-download.yaml"
    46  )
    47  
    48  // viperReader implements Reader using viper as backend for reading from environment variables
    49  // and from a clusterctl config file.
    50  type viperReader struct {
    51  	configPaths []string
    52  }
    53  
    54  type viperReaderOption func(*viperReader)
    55  
    56  func injectConfigPaths(configPaths []string) viperReaderOption {
    57  	return func(vr *viperReader) {
    58  		vr.configPaths = configPaths
    59  	}
    60  }
    61  
    62  // newViperReader returns a viperReader.
    63  func newViperReader(opts ...viperReaderOption) (Reader, error) {
    64  	configDirectory, err := xdg.ConfigFile(ConfigFolderXDG)
    65  	if err != nil {
    66  		return nil, err
    67  	}
    68  	vr := &viperReader{
    69  		configPaths: []string{configDirectory, filepath.Join(xdg.Home, ConfigFolder)},
    70  	}
    71  	for _, o := range opts {
    72  		o(vr)
    73  	}
    74  	return vr, nil
    75  }
    76  
    77  // Init initialize the viperReader.
    78  func (v *viperReader) Init(ctx context.Context, path string) error {
    79  	log := logf.Log
    80  
    81  	// Configure viper for reading environment variables as well, and more specifically:
    82  	// AutomaticEnv force viper to check for an environment variable any time a viper.Get request is made.
    83  	// It will check for a environment variable with a name matching the key uppercased; in case name use the - delimiter,
    84  	// the SetEnvKeyReplacer forces matching to name use the _ delimiter instead (- is not allowed in linux env variable names).
    85  	replacer := strings.NewReplacer("-", "_")
    86  	viper.SetEnvKeyReplacer(replacer)
    87  	viper.AllowEmptyEnv(true)
    88  	viper.AutomaticEnv()
    89  
    90  	if path != "" {
    91  		url, err := url.Parse(path)
    92  		if err != nil {
    93  			return errors.Wrap(err, "failed to url parse the config path")
    94  		}
    95  
    96  		switch {
    97  		case url.Scheme == "https" || url.Scheme == "http":
    98  			var configDirectory string
    99  			if len(v.configPaths) > 0 {
   100  				configDirectory = v.configPaths[0]
   101  			} else {
   102  				configDirectory, err = xdg.ConfigFile(ConfigFolderXDG)
   103  				if err != nil {
   104  					return err
   105  				}
   106  			}
   107  
   108  			downloadConfigFile := filepath.Join(configDirectory, DownloadConfigFile)
   109  			err = downloadFile(ctx, url.String(), downloadConfigFile)
   110  			if err != nil {
   111  				return err
   112  			}
   113  
   114  			viper.SetConfigFile(downloadConfigFile)
   115  		default:
   116  			if _, err := os.Stat(path); err != nil {
   117  				return errors.Wrap(err, "failed to check if clusterctl config file exists")
   118  			}
   119  			// Use path file from the flag.
   120  			viper.SetConfigFile(path)
   121  		}
   122  	} else {
   123  		// Checks if there is a default $XDG_CONFIG_HOME/cluster-api/clusterctl{.extension} or $HOME/.cluster-api/clusterctl{.extension} file
   124  		if !v.checkDefaultConfig() {
   125  			// since there is no default config to read from, just skip
   126  			// reading in config
   127  			log.V(5).Info("No default config file available")
   128  			return nil
   129  		}
   130  		// Configure viper for reading $XDG_CONFIG_HOME/cluster-api/clusterctl{.extension} or $HOME/.cluster-api/clusterctl{.extension} file
   131  		viper.SetConfigName(ConfigName)
   132  		for _, p := range v.configPaths {
   133  			viper.AddConfigPath(p)
   134  		}
   135  	}
   136  
   137  	if err := viper.ReadInConfig(); err != nil {
   138  		return err
   139  	}
   140  	log.V(5).Info("Using configuration", "File", viper.ConfigFileUsed())
   141  	return nil
   142  }
   143  
   144  func downloadFile(ctx context.Context, url string, filepath string) error {
   145  	// Create the file
   146  	out, err := os.Create(filepath) //nolint:gosec // No security issue: filepath is safe.
   147  	if err != nil {
   148  		return errors.Wrapf(err, "failed to create the clusterctl config file %s", filepath)
   149  	}
   150  	defer out.Close()
   151  
   152  	client := &http.Client{
   153  		Timeout: 30 * time.Second,
   154  	}
   155  	// Get the data
   156  	req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
   157  	if err != nil {
   158  		return errors.Wrapf(err, "failed to download the clusterctl config file from %s: failed to create request", url)
   159  	}
   160  
   161  	resp, err := client.Do(req)
   162  	if err != nil {
   163  		return errors.Wrapf(err, "failed to download the clusterctl config file from %s", url)
   164  	}
   165  	if resp.StatusCode != http.StatusOK {
   166  		return errors.Errorf("failed to download the clusterctl config file from %s got %d", url, resp.StatusCode)
   167  	}
   168  	defer resp.Body.Close()
   169  
   170  	// Write the body to file
   171  	_, err = io.Copy(out, resp.Body)
   172  	if err != nil {
   173  		return errors.Wrap(err, "failed to save the data in the clusterctl config")
   174  	}
   175  
   176  	return nil
   177  }
   178  
   179  func (v *viperReader) Get(key string) (string, error) {
   180  	if viper.Get(key) == nil {
   181  		return "", errors.Errorf("Failed to get value for variable %q. Please set the variable value using os env variables or using the .clusterctl config file", key)
   182  	}
   183  	return viper.GetString(key), nil
   184  }
   185  
   186  func (v *viperReader) Set(key, value string) {
   187  	viper.Set(key, value)
   188  }
   189  
   190  func (v *viperReader) UnmarshalKey(key string, rawval interface{}) error {
   191  	return viper.UnmarshalKey(key, rawval)
   192  }
   193  
   194  // checkDefaultConfig checks the existence of the default config.
   195  // Returns true if it finds a supported config file in the available config
   196  // folders.
   197  func (v *viperReader) checkDefaultConfig() bool {
   198  	for _, path := range v.configPaths {
   199  		for _, ext := range viper.SupportedExts {
   200  			f := filepath.Join(path, fmt.Sprintf("%s.%s", ConfigName, ext))
   201  			_, err := os.Stat(f)
   202  			if err == nil {
   203  				return true
   204  			}
   205  		}
   206  	}
   207  	return false
   208  }