k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/utils/image/manifest.go (about)

     1  /*
     2  Copyright 2017 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 image
    18  
    19  import (
    20  	"bufio"
    21  	"bytes"
    22  	"crypto/sha256"
    23  	"encoding/base64"
    24  	"fmt"
    25  	"io"
    26  	"net/http"
    27  	"os"
    28  	"regexp"
    29  	"strings"
    30  
    31  	"gopkg.in/yaml.v2"
    32  )
    33  
    34  // RegistryList holds public and private image registries
    35  type RegistryList struct {
    36  	GcAuthenticatedRegistry  string `yaml:"gcAuthenticatedRegistry"`
    37  	PromoterE2eRegistry      string `yaml:"promoterE2eRegistry"`
    38  	BuildImageRegistry       string `yaml:"buildImageRegistry"`
    39  	InvalidRegistry          string `yaml:"invalidRegistry"`
    40  	GcEtcdRegistry           string `yaml:"gcEtcdRegistry"`
    41  	GcRegistry               string `yaml:"gcRegistry"`
    42  	SigStorageRegistry       string `yaml:"sigStorageRegistry"`
    43  	PrivateRegistry          string `yaml:"privateRegistry"`
    44  	DockerLibraryRegistry    string `yaml:"dockerLibraryRegistry"`
    45  	CloudProviderGcpRegistry string `yaml:"cloudProviderGcpRegistry"`
    46  }
    47  
    48  // Config holds an images registry, name, and version
    49  type Config struct {
    50  	registry string
    51  	name     string
    52  	version  string
    53  }
    54  
    55  // SetRegistry sets an image registry in a Config struct
    56  func (i *Config) SetRegistry(registry string) {
    57  	i.registry = registry
    58  }
    59  
    60  // SetName sets an image name in a Config struct
    61  func (i *Config) SetName(name string) {
    62  	i.name = name
    63  }
    64  
    65  // SetVersion sets an image version in a Config struct
    66  func (i *Config) SetVersion(version string) {
    67  	i.version = version
    68  }
    69  
    70  func Init(repoList string) {
    71  	registry, imageConfigs, originalImageConfigs = readRepoList(repoList)
    72  }
    73  
    74  func readRepoList(repoList string) (RegistryList, map[ImageID]Config, map[ImageID]Config) {
    75  	registry := initRegistry
    76  
    77  	if repoList == "" {
    78  		imageConfigs, originalImageConfigs := initImageConfigs(registry)
    79  		return registry, imageConfigs, originalImageConfigs
    80  	}
    81  
    82  	var fileContent []byte
    83  	var err error
    84  	if strings.HasPrefix(repoList, "https://") || strings.HasPrefix(repoList, "http://") {
    85  		var b bytes.Buffer
    86  		err = readFromURL(repoList, bufio.NewWriter(&b))
    87  		if err != nil {
    88  			panic(fmt.Errorf("error reading '%v' url contents: %v", repoList, err))
    89  		}
    90  		fileContent = b.Bytes()
    91  	} else {
    92  		fileContent, err = os.ReadFile(repoList)
    93  		if err != nil {
    94  			panic(fmt.Errorf("error reading '%v' file contents: %v", repoList, err))
    95  		}
    96  	}
    97  
    98  	err = yaml.Unmarshal(fileContent, &registry)
    99  	if err != nil {
   100  		panic(fmt.Errorf("error unmarshalling '%v' YAML file: %v", repoList, err))
   101  	}
   102  
   103  	imageConfigs, originalImageConfigs := initImageConfigs(registry)
   104  
   105  	return registry, imageConfigs, originalImageConfigs
   106  
   107  }
   108  
   109  // Essentially curl url | writer
   110  func readFromURL(url string, writer io.Writer) error {
   111  	httpTransport := new(http.Transport)
   112  	httpTransport.Proxy = http.ProxyFromEnvironment
   113  
   114  	c := &http.Client{Transport: httpTransport}
   115  	r, err := c.Get(url)
   116  	if err != nil {
   117  		return err
   118  	}
   119  	defer r.Body.Close()
   120  	if r.StatusCode >= 400 {
   121  		return fmt.Errorf("%v returned %d", url, r.StatusCode)
   122  	}
   123  	_, err = io.Copy(writer, r.Body)
   124  	if err != nil {
   125  		return err
   126  	}
   127  	return nil
   128  }
   129  
   130  var (
   131  	initRegistry = RegistryList{
   132  		GcAuthenticatedRegistry:  "gcr.io/authenticated-image-pulling",
   133  		PromoterE2eRegistry:      "registry.k8s.io/e2e-test-images",
   134  		BuildImageRegistry:       "registry.k8s.io/build-image",
   135  		InvalidRegistry:          "invalid.registry.k8s.io/invalid",
   136  		GcEtcdRegistry:           "registry.k8s.io",
   137  		GcRegistry:               "registry.k8s.io",
   138  		SigStorageRegistry:       "registry.k8s.io/sig-storage",
   139  		PrivateRegistry:          "gcr.io/k8s-authenticated-test",
   140  		DockerLibraryRegistry:    "docker.io/library",
   141  		CloudProviderGcpRegistry: "registry.k8s.io/cloud-provider-gcp",
   142  	}
   143  
   144  	registry, imageConfigs, originalImageConfigs = readRepoList(os.Getenv("KUBE_TEST_REPO_LIST"))
   145  )
   146  
   147  type ImageID int
   148  
   149  const (
   150  	// None is to be used for unset/default images
   151  	None ImageID = iota
   152  	// Agnhost image
   153  	Agnhost
   154  	// AgnhostPrivate image
   155  	AgnhostPrivate
   156  	// APIServer image
   157  	APIServer
   158  	// AppArmorLoader image
   159  	AppArmorLoader
   160  	// AuthenticatedAlpine image
   161  	AuthenticatedAlpine
   162  	// AuthenticatedWindowsNanoServer image
   163  	AuthenticatedWindowsNanoServer
   164  	// BusyBox image
   165  	BusyBox
   166  	// CudaVectorAdd image
   167  	CudaVectorAdd
   168  	// CudaVectorAdd2 image
   169  	CudaVectorAdd2
   170  	// DistrolessIptables Image
   171  	DistrolessIptables
   172  	// Etcd image
   173  	Etcd
   174  	// Httpd image
   175  	Httpd
   176  	// HttpdNew image
   177  	HttpdNew
   178  	// InvalidRegistryImage image
   179  	InvalidRegistryImage
   180  	// IpcUtils image
   181  	IpcUtils
   182  	// JessieDnsutils image
   183  	JessieDnsutils
   184  	// Kitten image
   185  	Kitten
   186  	// Nautilus image
   187  	Nautilus
   188  	// NFSProvisioner image
   189  	NFSProvisioner
   190  	// Nginx image
   191  	Nginx
   192  	// NginxNew image
   193  	NginxNew
   194  	// NodePerfNpbEp image
   195  	NodePerfNpbEp
   196  	// NodePerfNpbIs image
   197  	NodePerfNpbIs
   198  	// NodePerfTfWideDeep image
   199  	NodePerfTfWideDeep
   200  	// Nonewprivs image
   201  	Nonewprivs
   202  	// NonRoot runs with a default user of 1234
   203  	NonRoot
   204  	// Pause - when these values are updated, also update cmd/kubelet/app/options/container_runtime.go
   205  	// Pause image
   206  	Pause
   207  	// Perl image
   208  	Perl
   209  	// PrometheusDummyExporter image
   210  	PrometheusDummyExporter
   211  	// PrometheusToSd image
   212  	PrometheusToSd
   213  	// Redis image
   214  	Redis
   215  	// RegressionIssue74839 image
   216  	RegressionIssue74839
   217  	// ResourceConsumer image
   218  	ResourceConsumer
   219  	// SdDummyExporter image
   220  	SdDummyExporter
   221  	// VolumeNFSServer image
   222  	VolumeNFSServer
   223  	// VolumeISCSIServer image
   224  	VolumeISCSIServer
   225  )
   226  
   227  func initImageConfigs(list RegistryList) (map[ImageID]Config, map[ImageID]Config) {
   228  	configs := map[ImageID]Config{}
   229  	configs[Agnhost] = Config{list.PromoterE2eRegistry, "agnhost", "2.52"}
   230  	configs[AgnhostPrivate] = Config{list.PrivateRegistry, "agnhost", "2.6"}
   231  	configs[AuthenticatedAlpine] = Config{list.GcAuthenticatedRegistry, "alpine", "3.7"}
   232  	configs[AuthenticatedWindowsNanoServer] = Config{list.GcAuthenticatedRegistry, "windows-nanoserver", "v1"}
   233  	configs[APIServer] = Config{list.PromoterE2eRegistry, "sample-apiserver", "1.29.2"}
   234  	configs[AppArmorLoader] = Config{list.PromoterE2eRegistry, "apparmor-loader", "1.4"}
   235  	configs[BusyBox] = Config{list.PromoterE2eRegistry, "busybox", "1.36.1-1"}
   236  	configs[CudaVectorAdd] = Config{list.PromoterE2eRegistry, "cuda-vector-add", "1.0"}
   237  	configs[CudaVectorAdd2] = Config{list.PromoterE2eRegistry, "cuda-vector-add", "2.3"}
   238  	configs[DistrolessIptables] = Config{list.BuildImageRegistry, "distroless-iptables", "v0.5.4"}
   239  	configs[Etcd] = Config{list.GcEtcdRegistry, "etcd", "3.5.13-0"}
   240  	configs[Httpd] = Config{list.PromoterE2eRegistry, "httpd", "2.4.38-4"}
   241  	configs[HttpdNew] = Config{list.PromoterE2eRegistry, "httpd", "2.4.39-4"}
   242  	configs[InvalidRegistryImage] = Config{list.InvalidRegistry, "alpine", "3.1"}
   243  	configs[IpcUtils] = Config{list.PromoterE2eRegistry, "ipc-utils", "1.3"}
   244  	configs[JessieDnsutils] = Config{list.PromoterE2eRegistry, "jessie-dnsutils", "1.7"}
   245  	configs[Kitten] = Config{list.PromoterE2eRegistry, "kitten", "1.7"}
   246  	configs[Nautilus] = Config{list.PromoterE2eRegistry, "nautilus", "1.7"}
   247  	configs[NFSProvisioner] = Config{list.SigStorageRegistry, "nfs-provisioner", "v4.0.8"}
   248  	configs[Nginx] = Config{list.PromoterE2eRegistry, "nginx", "1.14-4"}
   249  	configs[NginxNew] = Config{list.PromoterE2eRegistry, "nginx", "1.15-4"}
   250  	configs[NodePerfNpbEp] = Config{list.PromoterE2eRegistry, "node-perf/npb-ep", "1.2"}
   251  	configs[NodePerfNpbIs] = Config{list.PromoterE2eRegistry, "node-perf/npb-is", "1.2"}
   252  	configs[NodePerfTfWideDeep] = Config{list.PromoterE2eRegistry, "node-perf/tf-wide-deep", "1.3"}
   253  	configs[Nonewprivs] = Config{list.PromoterE2eRegistry, "nonewprivs", "1.3"}
   254  	configs[NonRoot] = Config{list.PromoterE2eRegistry, "nonroot", "1.4"}
   255  	// Pause - when these values are updated, also update cmd/kubelet/app/options/container_runtime.go
   256  	configs[Pause] = Config{list.GcRegistry, "pause", "3.9"}
   257  	configs[Perl] = Config{list.PromoterE2eRegistry, "perl", "5.26"}
   258  	configs[PrometheusDummyExporter] = Config{list.GcRegistry, "prometheus-dummy-exporter", "v0.1.0"}
   259  	configs[PrometheusToSd] = Config{list.GcRegistry, "prometheus-to-sd", "v0.5.0"}
   260  	configs[Redis] = Config{list.PromoterE2eRegistry, "redis", "5.0.5-3"}
   261  	configs[RegressionIssue74839] = Config{list.PromoterE2eRegistry, "regression-issue-74839", "1.2"}
   262  	configs[ResourceConsumer] = Config{list.PromoterE2eRegistry, "resource-consumer", "1.13"}
   263  	configs[SdDummyExporter] = Config{list.GcRegistry, "sd-dummy-exporter", "v0.2.0"}
   264  	configs[VolumeNFSServer] = Config{list.PromoterE2eRegistry, "volume/nfs", "1.4"}
   265  	configs[VolumeISCSIServer] = Config{list.PromoterE2eRegistry, "volume/iscsi", "2.6"}
   266  
   267  	// This adds more config entries. Those have no pre-defined ImageID number,
   268  	// but will be used via ReplaceRegistryInImageURL when deploying
   269  	// CSI drivers (test/e2e/storage/util/create.go).
   270  	appendCSIImageConfigs(configs)
   271  
   272  	// if requested, map all the SHAs into a known format based on the input
   273  	originalImageConfigs := configs
   274  	if repo := os.Getenv("KUBE_TEST_REPO"); len(repo) > 0 {
   275  		configs = GetMappedImageConfigs(originalImageConfigs, repo)
   276  	}
   277  
   278  	return configs, originalImageConfigs
   279  }
   280  
   281  // GetMappedImageConfigs returns the images if they were mapped to the provided
   282  // image repository.
   283  func GetMappedImageConfigs(originalImageConfigs map[ImageID]Config, repo string) map[ImageID]Config {
   284  	configs := make(map[ImageID]Config)
   285  	for i, config := range originalImageConfigs {
   286  		switch i {
   287  		case InvalidRegistryImage, AuthenticatedAlpine,
   288  			AuthenticatedWindowsNanoServer, AgnhostPrivate:
   289  			// These images are special and can't be run out of the cloud - some because they
   290  			// are authenticated, and others because they are not real images. Tests that depend
   291  			// on these images can't be run without access to the public internet.
   292  			configs[i] = config
   293  			continue
   294  		}
   295  
   296  		// Build a new tag with the ImageID, a hash of the image spec (to be unique) and
   297  		// shorten and make the pull spec "safe" so it will fit in the tag
   298  		configs[i] = getRepositoryMappedConfig(i, config, repo)
   299  	}
   300  	return configs
   301  }
   302  
   303  var (
   304  	reCharSafe = regexp.MustCompile(`[^\w]`)
   305  	reDashes   = regexp.MustCompile(`-+`)
   306  )
   307  
   308  // getRepositoryMappedConfig maps an existing image to the provided repo, generating a
   309  // tag that is unique with the input config. The tag will contain the imageID, a hash of
   310  // the image spec (to be unique) and shorten and make the pull spec "safe" so it will
   311  // fit in the tag to allow a human to recognize the value. If imageID is None, then no
   312  // imageID will be added to the tag.
   313  func getRepositoryMappedConfig(imageID ImageID, config Config, repo string) Config {
   314  	parts := strings.SplitN(repo, "/", 2)
   315  	registry, name := parts[0], parts[1]
   316  
   317  	pullSpec := config.GetE2EImage()
   318  
   319  	h := sha256.New()
   320  	h.Write([]byte(pullSpec))
   321  	hash := base64.RawURLEncoding.EncodeToString(h.Sum(nil))[:16]
   322  
   323  	shortName := reCharSafe.ReplaceAllLiteralString(pullSpec, "-")
   324  	shortName = reDashes.ReplaceAllLiteralString(shortName, "-")
   325  	maxLength := 127 - 16 - 6 - 10
   326  	if len(shortName) > maxLength {
   327  		shortName = shortName[len(shortName)-maxLength:]
   328  	}
   329  	var version string
   330  	if imageID == None {
   331  		version = fmt.Sprintf("e2e-%s-%s", shortName, hash)
   332  	} else {
   333  		version = fmt.Sprintf("e2e-%d-%s-%s", imageID, shortName, hash)
   334  	}
   335  
   336  	return Config{
   337  		registry: registry,
   338  		name:     name,
   339  		version:  version,
   340  	}
   341  }
   342  
   343  // GetOriginalImageConfigs returns the configuration before any mapping rules.
   344  func GetOriginalImageConfigs() map[ImageID]Config {
   345  	return originalImageConfigs
   346  }
   347  
   348  // GetImageConfigs returns the map of imageConfigs
   349  func GetImageConfigs() map[ImageID]Config {
   350  	return imageConfigs
   351  }
   352  
   353  // GetConfig returns the Config object for an image
   354  func GetConfig(image ImageID) Config {
   355  	return imageConfigs[image]
   356  }
   357  
   358  // GetE2EImage returns the fully qualified URI to an image (including version)
   359  func GetE2EImage(image ImageID) string {
   360  	return fmt.Sprintf("%s/%s:%s", imageConfigs[image].registry, imageConfigs[image].name, imageConfigs[image].version)
   361  }
   362  
   363  // GetE2EImage returns the fully qualified URI to an image (including version)
   364  func (i *Config) GetE2EImage() string {
   365  	return fmt.Sprintf("%s/%s:%s", i.registry, i.name, i.version)
   366  }
   367  
   368  // GetPauseImageName returns the pause image name with proper version
   369  func GetPauseImageName() string {
   370  	return GetE2EImage(Pause)
   371  }
   372  
   373  // ReplaceRegistryInImageURL replaces the registry in the image URL with a custom one based
   374  // on the configured registries.
   375  func ReplaceRegistryInImageURL(imageURL string) (string, error) {
   376  	return replaceRegistryInImageURLWithList(imageURL, registry)
   377  }
   378  
   379  // replaceRegistryInImageURLWithList replaces the registry in the image URL with a custom one based
   380  // on the given registry list.
   381  func replaceRegistryInImageURLWithList(imageURL string, reg RegistryList) (string, error) {
   382  	parts := strings.Split(imageURL, "/")
   383  	countParts := len(parts)
   384  	registryAndUser := strings.Join(parts[:countParts-1], "/")
   385  
   386  	if repo := os.Getenv("KUBE_TEST_REPO"); len(repo) > 0 {
   387  		imageID := None
   388  		for i, v := range originalImageConfigs {
   389  			if v.GetE2EImage() == imageURL {
   390  				imageID = i
   391  				break
   392  			}
   393  		}
   394  		last := strings.SplitN(parts[countParts-1], ":", 2)
   395  		if len(last) == 1 {
   396  			return "", fmt.Errorf("image %q is required to be in an image:tag format", imageURL)
   397  		}
   398  		config := getRepositoryMappedConfig(imageID, Config{
   399  			registry: parts[0],
   400  			name:     strings.Join([]string{strings.Join(parts[1:countParts-1], "/"), last[0]}, "/"),
   401  			version:  last[1],
   402  		}, repo)
   403  		return config.GetE2EImage(), nil
   404  	}
   405  
   406  	switch registryAndUser {
   407  	case initRegistry.GcRegistry:
   408  		registryAndUser = reg.GcRegistry
   409  	case initRegistry.SigStorageRegistry:
   410  		registryAndUser = reg.SigStorageRegistry
   411  	case initRegistry.PrivateRegistry:
   412  		registryAndUser = reg.PrivateRegistry
   413  	case initRegistry.InvalidRegistry:
   414  		registryAndUser = reg.InvalidRegistry
   415  	case initRegistry.PromoterE2eRegistry:
   416  		registryAndUser = reg.PromoterE2eRegistry
   417  	case initRegistry.BuildImageRegistry:
   418  		registryAndUser = reg.BuildImageRegistry
   419  	case initRegistry.GcAuthenticatedRegistry:
   420  		registryAndUser = reg.GcAuthenticatedRegistry
   421  	case initRegistry.DockerLibraryRegistry:
   422  		registryAndUser = reg.DockerLibraryRegistry
   423  	case initRegistry.CloudProviderGcpRegistry:
   424  		registryAndUser = reg.CloudProviderGcpRegistry
   425  	default:
   426  		if countParts == 1 {
   427  			// We assume we found an image from docker hub library
   428  			// e.g. openjdk -> docker.io/library/openjdk
   429  			registryAndUser = reg.DockerLibraryRegistry
   430  			break
   431  		}
   432  
   433  		return "", fmt.Errorf("Registry: %s is missing in test/utils/image/manifest.go, please add the registry, otherwise the test will fail on air-gapped clusters", registryAndUser)
   434  	}
   435  
   436  	return fmt.Sprintf("%s/%s", registryAndUser, parts[countParts-1]), nil
   437  }