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 }