Skip to content

Commit

Permalink
Merge pull request #2869 from nader-ziada/util-lib
Browse files Browse the repository at this point in the history
✨Add a library to parse container image name and other fields
  • Loading branch information
k8s-ci-robot authored Apr 9, 2020
2 parents 262e9b3 + 65dc85f commit efe6b70
Show file tree
Hide file tree
Showing 8 changed files with 394 additions and 147 deletions.
46 changes: 6 additions & 40 deletions cmd/clusterctl/client/config/imagemeta_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,10 @@ limitations under the License.
package config

import (
"fmt"
"strings"

"github.com/docker/distribution/reference"
"github.com/pkg/errors"
"sigs.k8s.io/cluster-api/util/container"
)

const (
Expand Down Expand Up @@ -119,53 +118,20 @@ func (i *imageMeta) Union(other *imageMeta) {

// ApplyToImage changes an image name applying the transformations defined in the current imageMeta.
func (i *imageMeta) ApplyToImage(image string) (string, error) {
// Splits the image name into its own composing parts
ref, err := reference.ParseNormalizedNamed(image)

newImage, err := container.ImageFromString(image)
if err != nil {
return "", err
}

// apply transformations
if i.Repository != "" {
// store tag & digest for rebuilding the image name
tagged, hasTag := ref.(reference.Tagged)
digested, hasDigest := ref.(reference.Digested)

// detect the image name, dropping host and path if any
name := ref.Name()
imageNameIndex := strings.LastIndex(name, "/")
if imageNameIndex != -1 {
name = strings.TrimPrefix(name[imageNameIndex+1:], "/")
}

// parse the new image resulting by concatenating the new repository and the image name
ref, err = reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", strings.TrimSuffix(i.Repository, "/"), name))
if err != nil {
return "", err
}

// applies back tag & digest
if hasTag {
ref, err = reference.WithTag(ref, tagged.Tag())
if err != nil {
return "", err
}
}

if hasDigest {
ref, err = reference.WithDigest(ref, digested.Digest())
if err != nil {
return "", err
}
}
newImage.Repository = strings.TrimSuffix(i.Repository, "/")
}
if i.Tag != "" {
ref, err = reference.WithTag(reference.TrimNamed(ref), i.Tag)
if err != nil {
return "", err
}
newImage.Tag = i.Tag
}

// returns the resulting image name
return ref.String(), nil
return newImage.String(), nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/container"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)
Expand Down Expand Up @@ -252,7 +253,7 @@ func (in *KubeadmControlPlane) validateCoreDNSImage() (allErrs field.ErrorList)
return allErrs
}
// TODO: Remove when kubeadm types include OpenAPI validation
if !util.ImageTagIsValid(in.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag) {
if !container.ImageTagIsValid(in.Spec.KubeadmConfigSpec.ClusterConfiguration.DNS.ImageTag) {
allErrs = append(
allErrs,
field.Forbidden(
Expand Down Expand Up @@ -320,7 +321,7 @@ func (in *KubeadmControlPlane) validateEtcd(prev *KubeadmControlPlane) (allErrs
}

// TODO: Remove when kubeadm types include OpenAPI validation
if in.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local != nil && !util.ImageTagIsValid(in.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ImageTag) {
if in.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local != nil && !container.ImageTagIsValid(in.Spec.KubeadmConfigSpec.ClusterConfiguration.Etcd.Local.ImageTag) {
allErrs = append(
allErrs,
field.Forbidden(
Expand Down
5 changes: 3 additions & 2 deletions controlplane/kubeadm/internal/workload_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import (
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha3"
"sigs.k8s.io/cluster-api/util"
"sigs.k8s.io/cluster-api/util/certs"
containerutil "sigs.k8s.io/cluster-api/util/container"
"sigs.k8s.io/cluster-api/util/patch"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -452,13 +453,13 @@ func (w *Workload) UpdateKubeProxyImageInfo(ctx context.Context, kcp *controlpla
return nil
}

newImageName, err := util.ModifyImageTag(container.Image, kcp.Spec.Version)
newImageName, err := containerutil.ModifyImageTag(container.Image, kcp.Spec.Version)
if err != nil {
return err
}
if kcp.Spec.KubeadmConfigSpec.ClusterConfiguration != nil &&
kcp.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository != "" {
newImageName, err = util.ModifyImageRepository(newImageName, kcp.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository)
newImageName, err = containerutil.ModifyImageRepository(newImageName, kcp.Spec.KubeadmConfigSpec.ClusterConfiguration.ImageRepository)
if err != nil {
return err
}
Expand Down
15 changes: 7 additions & 8 deletions controlplane/kubeadm/internal/workload_cluster_coredns.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import (
"fmt"

"github.com/coredns/corefile-migration/migration"
"github.com/docker/distribution/reference"
"github.com/pkg/errors"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
Expand All @@ -30,6 +29,7 @@ import (
kubeadmv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/types/v1beta1"
controlplanev1 "sigs.k8s.io/cluster-api/controlplane/kubeadm/api/v1alpha3"
"sigs.k8s.io/cluster-api/util"
containerutil "sigs.k8s.io/cluster-api/util/container"
"sigs.k8s.io/cluster-api/util/patch"
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
)
Expand Down Expand Up @@ -143,13 +143,13 @@ func (w *Workload) getCoreDNSInfo(ctx context.Context, clusterConfig *kubeadmv1.
}

// Parse container image.
parsedImage, err := reference.ParseNormalizedNamed(container.Image)
parsedImage, err := containerutil.ImageFromString(container.Image)
if err != nil {
return nil, errors.Wrapf(err, "unable to parse %q deployment image", container.Image)
}

// Handle imageRepository.
toImageRepository := fmt.Sprintf("%s/%s", reference.Domain(parsedImage), reference.Path(parsedImage))
toImageRepository := fmt.Sprintf("%s/%s", parsedImage.Repository, parsedImage.Name)
if clusterConfig.ImageRepository != "" {
toImageRepository = fmt.Sprintf("%s/%s", clusterConfig.ImageRepository, coreDNSKey)
}
Expand All @@ -158,15 +158,14 @@ func (w *Workload) getCoreDNSInfo(ctx context.Context, clusterConfig *kubeadmv1.
}

// Handle imageTag.
imageRefTag, ok := parsedImage.(reference.Tagged)
if !ok {
if parsedImage.Tag == "" {
return nil, errors.Errorf("failed to update coredns deployment: does not have a valid image tag: %q", container.Image)
}
currentMajorMinorPatch, err := extractImageVersion(imageRefTag.Tag())
currentMajorMinorPatch, err := extractImageVersion(parsedImage.Tag)
if err != nil {
return nil, err
}
toImageTag := imageRefTag.Tag()
toImageTag := parsedImage.Tag
if clusterConfig.DNS.ImageTag != "" {
toImageTag = clusterConfig.DNS.ImageTag
}
Expand All @@ -180,7 +179,7 @@ func (w *Workload) getCoreDNSInfo(ctx context.Context, clusterConfig *kubeadmv1.
Deployment: deployment,
CurrentMajorMinorPatch: currentMajorMinorPatch,
TargetMajorMinorPatch: targetMajorMinorPatch,
FromImageTag: imageRefTag.Tag(),
FromImageTag: parsedImage.Tag,
ToImageTag: toImageTag,
FromImage: container.Image,
ToImage: fmt.Sprintf("%s:%s", toImageRepository, toImageTag),
Expand Down
141 changes: 141 additions & 0 deletions util/container/image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
/*
Copyright 2020 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package container

import (
"fmt"
"path"
"regexp"

// Import the crypto sha256 algorithm for the docker image parser to work
_ "crypto/sha256"
// Import the crypto/sha512 algorithm for the docker image parser to work with 384 and 512 sha hashes
_ "crypto/sha512"

"github.com/docker/distribution/reference"
"github.com/pkg/errors"
)

var (
ociTagAllowedChars = regexp.MustCompile(`[^-a-zA-Z0-9_\.]`)
)

// Image type represents the container image details
type Image struct {
Repository string
Name string
Tag string
Digest string
}

// ImageFromString parses a docker image string into three parts: repo, tag and digest.
func ImageFromString(image string) (Image, error) {
named, err := reference.ParseNamed(image)
if err != nil {
return Image{}, fmt.Errorf("couldn't parse image name: %v", err)
}

var repo, tag, digest string
_, nameOnly := path.Split(reference.Path(named))
if nameOnly != "" {
// split out the part of the name after the last /
lenOfCompleteName := len(named.Name())
repo = named.Name()[:lenOfCompleteName-len(nameOnly)-1]
}

tagged, ok := named.(reference.Tagged)
if ok {
tag = tagged.Tag()
}

digested, ok := named.(reference.Digested)
if ok {
digest = digested.Digest().String()
}

return Image{Repository: repo, Name: nameOnly, Tag: tag, Digest: digest}, nil
}

func (i Image) String() string {
// repo/name [ ":" tag ] [ "@" digest ]
ref := fmt.Sprintf("%s/%s", i.Repository, i.Name)
if i.Tag != "" {
ref = fmt.Sprintf("%s:%s", ref, i.Tag)
}
if i.Digest != "" {
ref = fmt.Sprintf("%s@%s", ref, i.Digest)
}
return ref
}

// ModifyImageRepository takes an imageName (e.g., repository/image:tag), and returns an image name with updated repository
func ModifyImageRepository(imageName, repositoryName string) (string, error) {
image, err := ImageFromString(imageName)
if err != nil {
return "", errors.Wrap(err, "failed to parse image name")
}
nameUpdated, err := reference.WithName(path.Join(repositoryName, image.Name))
if err != nil {
return "", errors.Wrap(err, "failed to update repository name")
}
if image.Tag != "" {
retagged, err := reference.WithTag(nameUpdated, image.Tag)
if err != nil {
return "", errors.Wrap(err, "failed to parse image tag")
}
return reference.FamiliarString(retagged), nil
}
return "", errors.New("image must be tagged")
}

// ModifyImageTag takes an imageName (e.g., repository/image:tag), and returns an image name with updated tag
func ModifyImageTag(imageName, tagName string) (string, error) {
normalisedTagName := SemverToOCIImageTag(tagName)

namedRef, err := reference.ParseNormalizedNamed(imageName)
if err != nil {
return "", errors.Wrap(err, "failed to parse image name")
}
// return error if images use digest as version instead of tag
if _, isCanonical := namedRef.(reference.Canonical); isCanonical {
return "", errors.New("image uses digest as version, cannot update tag ")
}

// update the image tag with tagName
namedTagged, err := reference.WithTag(namedRef, normalisedTagName)
if err != nil {
return "", errors.Wrap(err, "failed to update image tag")
}

return reference.FamiliarString(reference.TagNameOnly(namedTagged)), nil
}

// ImageTagIsValid ensures that a given image tag is compliant with the OCI spec
func ImageTagIsValid(tagName string) bool {
return !ociTagAllowedChars.MatchString(tagName)
}

// SemverToOCIImageTag is a helper function that replaces all
// non-allowed symbols in tag strings with underscores.
// Image tag can only contain lowercase and uppercase letters, digits,
// underscores, periods and dashes.
// Current usage is for CI images where all of symbols except '+' are valid,
// but function is for generic usage where input can't be always pre-validated.
// Taken from k8s.io/cmd/kubeadm/app/util
func SemverToOCIImageTag(version string) string {
return ociTagAllowedChars.ReplaceAllString(version, "_")
}
Loading

0 comments on commit efe6b70

Please sign in to comment.