sigs.k8s.io/cluster-api@v1.7.1/util/container/image.go (about)

     1  /*
     2  Copyright 2020 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 container implements container utility functionality.
    18  package container
    19  
    20  import (
    21  	_ "crypto/sha256" // Import the crypto/sha256 algorithm for the docker image parser to work with sha256 hashes.
    22  	_ "crypto/sha512" // Import the crypto/sha512 algorithm for the docker image parser to work with 384 and 512 sha hashes.
    23  	"fmt"
    24  	"path"
    25  	"regexp"
    26  
    27  	"github.com/distribution/reference"
    28  	"github.com/pkg/errors"
    29  )
    30  
    31  var (
    32  	ociTagAllowedChars = regexp.MustCompile(`[^-a-zA-Z0-9_\.]`)
    33  )
    34  
    35  // Image type represents the container image details.
    36  type Image struct {
    37  	Repository string
    38  	Name       string
    39  	Tag        string
    40  	Digest     string
    41  }
    42  
    43  // ImageFromString parses a docker image string into three parts: repo, tag and digest.
    44  func ImageFromString(image string) (Image, error) {
    45  	named, err := reference.ParseNamed(image)
    46  	if err != nil {
    47  		return Image{}, fmt.Errorf("couldn't parse image name: %v", err)
    48  	}
    49  
    50  	var repo, tag, digest string
    51  	_, nameOnly := path.Split(reference.Path(named))
    52  	if nameOnly != "" {
    53  		// split out the part of the name after the last /
    54  		lenOfCompleteName := len(named.Name())
    55  		repo = named.Name()[:lenOfCompleteName-len(nameOnly)-1]
    56  	}
    57  
    58  	tagged, ok := named.(reference.Tagged)
    59  	if ok {
    60  		tag = tagged.Tag()
    61  	}
    62  
    63  	digested, ok := named.(reference.Digested)
    64  	if ok {
    65  		digest = digested.Digest().String()
    66  	}
    67  
    68  	return Image{Repository: repo, Name: nameOnly, Tag: tag, Digest: digest}, nil
    69  }
    70  
    71  func (i Image) String() string {
    72  	// repo/name [ ":" tag ] [ "@" digest ]
    73  	ref := fmt.Sprintf("%s/%s", i.Repository, i.Name)
    74  	if i.Tag != "" {
    75  		ref = fmt.Sprintf("%s:%s", ref, i.Tag)
    76  	}
    77  	if i.Digest != "" {
    78  		ref = fmt.Sprintf("%s@%s", ref, i.Digest)
    79  	}
    80  	return ref
    81  }
    82  
    83  // ModifyImageRepository takes an imageName (e.g., repository/image:tag), and returns an image name with updated repository.
    84  func ModifyImageRepository(imageName, repositoryName string) (string, error) {
    85  	image, err := ImageFromString(imageName)
    86  	if err != nil {
    87  		return "", errors.Wrap(err, "failed to parse image name")
    88  	}
    89  	nameUpdated, err := reference.WithName(path.Join(repositoryName, image.Name))
    90  	if err != nil {
    91  		return "", errors.Wrap(err, "failed to update repository name")
    92  	}
    93  	if image.Tag != "" {
    94  		retagged, err := reference.WithTag(nameUpdated, image.Tag)
    95  		if err != nil {
    96  			return "", errors.Wrap(err, "failed to parse image tag")
    97  		}
    98  		return reference.FamiliarString(retagged), nil
    99  	}
   100  	return "", errors.New("image must be tagged")
   101  }
   102  
   103  // ModifyImageTag takes an imageName (e.g., repository/image:tag), and returns an image name with updated tag.
   104  func ModifyImageTag(imageName, tagName string) (string, error) {
   105  	normalisedTagName := SemverToOCIImageTag(tagName)
   106  
   107  	namedRef, err := reference.ParseNormalizedNamed(imageName)
   108  	if err != nil {
   109  		return "", errors.Wrap(err, "failed to parse image name")
   110  	}
   111  	// return error if images use digest as version instead of tag
   112  	if _, isCanonical := namedRef.(reference.Canonical); isCanonical {
   113  		return "", errors.New("image uses digest as version, cannot update tag ")
   114  	}
   115  
   116  	// update the image tag with tagName
   117  	namedTagged, err := reference.WithTag(namedRef, normalisedTagName)
   118  	if err != nil {
   119  		return "", errors.Wrap(err, "failed to update image tag")
   120  	}
   121  
   122  	return reference.TagNameOnly(namedTagged).String(), nil
   123  }
   124  
   125  // ImageTagIsValid ensures that a given image tag is compliant with the OCI spec.
   126  func ImageTagIsValid(tagName string) bool {
   127  	return !ociTagAllowedChars.MatchString(tagName)
   128  }
   129  
   130  // SemverToOCIImageTag is a helper function that replaces all
   131  // non-allowed symbols in tag strings with underscores.
   132  // Image tag can only contain lowercase and uppercase letters, digits,
   133  // underscores, periods and dashes.
   134  // Current usage is for CI images where all of symbols except '+' are valid,
   135  // but function is for generic usage where input can't be always pre-validated.
   136  // Taken from k8s.io/cmd/kubeadm/app/util.
   137  func SemverToOCIImageTag(version string) string {
   138  	return ociTagAllowedChars.ReplaceAllString(version, "_")
   139  }