...

Source file src/k8s.io/kubernetes/pkg/credentialprovider/config.go

Documentation: k8s.io/kubernetes/pkg/credentialprovider

     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  }
   320  

View as plain text