k8s.io/kubernetes@v1.29.3/pkg/credentialprovider/config.go (about) 1 /* 2 Copyright 2014 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 credentialprovider 18 19 import ( 20 "encoding/base64" 21 "encoding/json" 22 "errors" 23 "fmt" 24 "io" 25 "net/http" 26 "os" 27 "path/filepath" 28 "strings" 29 "sync" 30 31 "k8s.io/klog/v2" 32 ) 33 34 const ( 35 maxReadLength = 10 * 1 << 20 // 10MB 36 ) 37 38 // DockerConfigJSON represents ~/.docker/config.json file info 39 // see https://github.com/docker/docker/pull/12009 40 type DockerConfigJSON struct { 41 Auths DockerConfig `json:"auths"` 42 // +optional 43 HTTPHeaders map[string]string `json:"HttpHeaders,omitempty"` 44 } 45 46 // DockerConfig represents the config file used by the docker CLI. 47 // This config that represents the credentials that should be used 48 // when pulling images from specific image repositories. 49 type DockerConfig map[string]DockerConfigEntry 50 51 // DockerConfigEntry wraps a docker config as a entry 52 type DockerConfigEntry struct { 53 Username string 54 Password string 55 Email string 56 Provider DockerConfigProvider 57 } 58 59 var ( 60 preferredPathLock sync.Mutex 61 preferredPath = "" 62 workingDirPath = "" 63 homeDirPath, _ = os.UserHomeDir() 64 rootDirPath = "/" 65 homeJSONDirPath = filepath.Join(homeDirPath, ".docker") 66 rootJSONDirPath = filepath.Join(rootDirPath, ".docker") 67 68 configFileName = ".dockercfg" 69 configJSONFileName = "config.json" 70 ) 71 72 // SetPreferredDockercfgPath set preferred docker config path 73 func SetPreferredDockercfgPath(path string) { 74 preferredPathLock.Lock() 75 defer preferredPathLock.Unlock() 76 preferredPath = path 77 } 78 79 // GetPreferredDockercfgPath get preferred docker config path 80 func GetPreferredDockercfgPath() string { 81 preferredPathLock.Lock() 82 defer preferredPathLock.Unlock() 83 return preferredPath 84 } 85 86 // DefaultDockercfgPaths returns default search paths of .dockercfg 87 func DefaultDockercfgPaths() []string { 88 return []string{GetPreferredDockercfgPath(), workingDirPath, homeDirPath, rootDirPath} 89 } 90 91 // DefaultDockerConfigJSONPaths returns default search paths of .docker/config.json 92 func DefaultDockerConfigJSONPaths() []string { 93 return []string{GetPreferredDockercfgPath(), workingDirPath, homeJSONDirPath, rootJSONDirPath} 94 } 95 96 // ReadDockercfgFile attempts to read a legacy dockercfg file from the given paths. 97 // if searchPaths is empty, the default paths are used. 98 func ReadDockercfgFile(searchPaths []string) (cfg DockerConfig, err error) { 99 if len(searchPaths) == 0 { 100 searchPaths = DefaultDockercfgPaths() 101 } 102 103 for _, configPath := range searchPaths { 104 absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configFileName)) 105 if err != nil { 106 klog.Errorf("while trying to canonicalize %s: %v", configPath, err) 107 continue 108 } 109 klog.V(4).Infof("looking for .dockercfg at %s", absDockerConfigFileLocation) 110 contents, err := os.ReadFile(absDockerConfigFileLocation) 111 if os.IsNotExist(err) { 112 continue 113 } 114 if err != nil { 115 klog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err) 116 continue 117 } 118 cfg, err := ReadDockerConfigFileFromBytes(contents) 119 if err != nil { 120 klog.V(4).Infof("couldn't get the config from %q contents: %v", absDockerConfigFileLocation, err) 121 continue 122 } 123 124 klog.V(4).Infof("found .dockercfg at %s", absDockerConfigFileLocation) 125 return cfg, nil 126 127 } 128 return nil, fmt.Errorf("couldn't find valid .dockercfg after checking in %v", searchPaths) 129 } 130 131 // ReadDockerConfigJSONFile attempts to read a docker config.json file from the given paths. 132 // if searchPaths is empty, the default paths are used. 133 func ReadDockerConfigJSONFile(searchPaths []string) (cfg DockerConfig, err error) { 134 if len(searchPaths) == 0 { 135 searchPaths = DefaultDockerConfigJSONPaths() 136 } 137 for _, configPath := range searchPaths { 138 absDockerConfigFileLocation, err := filepath.Abs(filepath.Join(configPath, configJSONFileName)) 139 if err != nil { 140 klog.Errorf("while trying to canonicalize %s: %v", configPath, err) 141 continue 142 } 143 klog.V(4).Infof("looking for %s at %s", configJSONFileName, absDockerConfigFileLocation) 144 cfg, err = ReadSpecificDockerConfigJSONFile(absDockerConfigFileLocation) 145 if err != nil { 146 if !os.IsNotExist(err) { 147 klog.V(4).Infof("while trying to read %s: %v", absDockerConfigFileLocation, err) 148 } 149 continue 150 } 151 klog.V(4).Infof("found valid %s at %s", configJSONFileName, absDockerConfigFileLocation) 152 return cfg, nil 153 } 154 return nil, fmt.Errorf("couldn't find valid %s after checking in %v", configJSONFileName, searchPaths) 155 156 } 157 158 // ReadSpecificDockerConfigJSONFile attempts to read docker configJSON from a given file path. 159 func ReadSpecificDockerConfigJSONFile(filePath string) (cfg DockerConfig, err error) { 160 var contents []byte 161 162 if contents, err = os.ReadFile(filePath); err != nil { 163 return nil, err 164 } 165 return readDockerConfigJSONFileFromBytes(contents) 166 } 167 168 // ReadDockerConfigFile read a docker config file from default path 169 func ReadDockerConfigFile() (cfg DockerConfig, err error) { 170 if cfg, err := ReadDockerConfigJSONFile(nil); err == nil { 171 return cfg, nil 172 } 173 // Can't find latest config file so check for the old one 174 return ReadDockercfgFile(nil) 175 } 176 177 // HTTPError wraps a non-StatusOK error code as an error. 178 type HTTPError struct { 179 StatusCode int 180 URL string 181 } 182 183 // Error implements error 184 func (he *HTTPError) Error() string { 185 return fmt.Sprintf("http status code: %d while fetching url %s", 186 he.StatusCode, he.URL) 187 } 188 189 // ReadURL read contents from given url 190 func ReadURL(url string, client *http.Client, header *http.Header) (body []byte, err error) { 191 req, err := http.NewRequest("GET", url, nil) 192 if err != nil { 193 return nil, err 194 } 195 if header != nil { 196 req.Header = *header 197 } 198 resp, err := client.Do(req) 199 if err != nil { 200 return nil, err 201 } 202 defer resp.Body.Close() 203 204 if resp.StatusCode != http.StatusOK { 205 klog.V(2).InfoS("Failed to read URL", "statusCode", resp.StatusCode, "URL", url) 206 return nil, &HTTPError{ 207 StatusCode: resp.StatusCode, 208 URL: url, 209 } 210 } 211 212 limitedReader := &io.LimitedReader{R: resp.Body, N: maxReadLength} 213 contents, err := io.ReadAll(limitedReader) 214 if err != nil { 215 return nil, err 216 } 217 218 if limitedReader.N <= 0 { 219 return nil, errors.New("the read limit is reached") 220 } 221 222 return contents, nil 223 } 224 225 // ReadDockerConfigFileFromBytes read a docker config file from the given bytes 226 func ReadDockerConfigFileFromBytes(contents []byte) (cfg DockerConfig, err error) { 227 if err = json.Unmarshal(contents, &cfg); err != nil { 228 return nil, errors.New("error occurred while trying to unmarshal json") 229 } 230 return 231 } 232 233 func readDockerConfigJSONFileFromBytes(contents []byte) (cfg DockerConfig, err error) { 234 var cfgJSON DockerConfigJSON 235 if err = json.Unmarshal(contents, &cfgJSON); err != nil { 236 return nil, errors.New("error occurred while trying to unmarshal json") 237 } 238 cfg = cfgJSON.Auths 239 return 240 } 241 242 // dockerConfigEntryWithAuth is used solely for deserializing the Auth field 243 // into a dockerConfigEntry during JSON deserialization. 244 type dockerConfigEntryWithAuth struct { 245 // +optional 246 Username string `json:"username,omitempty"` 247 // +optional 248 Password string `json:"password,omitempty"` 249 // +optional 250 Email string `json:"email,omitempty"` 251 // +optional 252 Auth string `json:"auth,omitempty"` 253 } 254 255 // UnmarshalJSON implements the json.Unmarshaler interface. 256 func (ident *DockerConfigEntry) UnmarshalJSON(data []byte) error { 257 var tmp dockerConfigEntryWithAuth 258 err := json.Unmarshal(data, &tmp) 259 if err != nil { 260 return err 261 } 262 263 ident.Username = tmp.Username 264 ident.Password = tmp.Password 265 ident.Email = tmp.Email 266 267 if len(tmp.Auth) == 0 { 268 return nil 269 } 270 271 ident.Username, ident.Password, err = decodeDockerConfigFieldAuth(tmp.Auth) 272 return err 273 } 274 275 // MarshalJSON implements the json.Marshaler interface. 276 func (ident DockerConfigEntry) MarshalJSON() ([]byte, error) { 277 toEncode := dockerConfigEntryWithAuth{ident.Username, ident.Password, ident.Email, ""} 278 toEncode.Auth = encodeDockerConfigFieldAuth(ident.Username, ident.Password) 279 280 return json.Marshal(toEncode) 281 } 282 283 // decodeDockerConfigFieldAuth deserializes the "auth" field from dockercfg into a 284 // username and a password. The format of the auth field is base64(<username>:<password>). 285 func decodeDockerConfigFieldAuth(field string) (username, password string, err error) { 286 287 var decoded []byte 288 289 // StdEncoding can only decode padded string 290 // RawStdEncoding can only decode unpadded string 291 if strings.HasSuffix(strings.TrimSpace(field), "=") { 292 // decode padded data 293 decoded, err = base64.StdEncoding.DecodeString(field) 294 } else { 295 // decode unpadded data 296 decoded, err = base64.RawStdEncoding.DecodeString(field) 297 } 298 299 if err != nil { 300 return 301 } 302 303 parts := strings.SplitN(string(decoded), ":", 2) 304 if len(parts) != 2 { 305 err = fmt.Errorf("unable to parse auth field, must be formatted as base64(username:password)") 306 return 307 } 308 309 username = parts[0] 310 password = parts[1] 311 312 return 313 } 314 315 func encodeDockerConfigFieldAuth(username, password string) string { 316 fieldValue := username + ":" + password 317 318 return base64.StdEncoding.EncodeToString([]byte(fieldValue)) 319 }