github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/manifest/images.go (about)

     1  /*
     2  Copyright 2019 The Skaffold 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 manifest
    18  
    19  import (
    20  	"context"
    21  	"strconv"
    22  
    23  	apimachinery "k8s.io/apimachinery/pkg/runtime/schema"
    24  
    25  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    26  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    27  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/instrumentation"
    28  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    30  )
    31  
    32  const imageField = "image"
    33  
    34  type ResourceSelectorImages struct {
    35  	allowlist map[apimachinery.GroupKind]latest.ResourceFilter
    36  	denylist  map[apimachinery.GroupKind]latest.ResourceFilter
    37  }
    38  
    39  func NewResourceSelectorImages(allowlist map[apimachinery.GroupKind]latest.ResourceFilter, denylist map[apimachinery.GroupKind]latest.ResourceFilter) *ResourceSelectorImages {
    40  	return &ResourceSelectorImages{
    41  		allowlist: allowlist,
    42  		denylist:  denylist,
    43  	}
    44  }
    45  
    46  func (rsi *ResourceSelectorImages) allowByGroupKind(gk apimachinery.GroupKind) bool {
    47  	if _, allowed := rsi.allowlist[gk]; allowed {
    48  		// TODO(aaron-prindle) see if it makes sense to make this only use the allowlist...
    49  		if rf, disallowed := rsi.denylist[gk]; disallowed {
    50  			for _, s := range rf.Labels {
    51  				if s == ".*" {
    52  					return false
    53  				}
    54  			}
    55  			for _, s := range rf.Image {
    56  				if s == ".*" {
    57  					return false
    58  				}
    59  			}
    60  		}
    61  		return true
    62  	}
    63  	return false
    64  }
    65  
    66  func (rsi *ResourceSelectorImages) allowByNavpath(gk apimachinery.GroupKind, navpath string, k string) (string, bool) {
    67  	matchedConfigConnectorImage := false
    68  
    69  	for _, w := range ConfigConnectorResourceSelector {
    70  		if k == imageField && w.Matches(gk.Group, gk.Kind) {
    71  			matchedConfigConnectorImage = true
    72  			break
    73  		}
    74  	}
    75  
    76  	if rf, ok := rsi.denylist[gk]; ok {
    77  		for _, denypath := range rf.Image {
    78  			if denypath == ".*" {
    79  				return "", false
    80  			}
    81  			if navpath == denypath {
    82  				return "", false
    83  			}
    84  		}
    85  	}
    86  
    87  	if rf, ok := rsi.allowlist[gk]; ok {
    88  		matchedConfigConnectorImage = false
    89  
    90  		for _, allowpath := range rf.Image {
    91  			if allowpath == ".*" && k == imageField {
    92  				return "", true
    93  			}
    94  			if navpath == allowpath {
    95  				return "", true
    96  			}
    97  		}
    98  	}
    99  	return "", matchedConfigConnectorImage
   100  }
   101  
   102  // GetImages gathers a map of base image names to the image with its tag
   103  func (l *ManifestList) GetImages(rs ResourceSelector) ([]graph.Artifact, error) {
   104  	s := &imageSaver{}
   105  	_, err := l.Visit(s, rs)
   106  	return s.Images, parseImagesInManifestErr(err)
   107  }
   108  
   109  type imageSaver struct {
   110  	Images []graph.Artifact
   111  }
   112  
   113  func (is *imageSaver) Visit(gk apimachinery.GroupKind, navpath string, o map[string]interface{}, k string, v interface{}, rs ResourceSelector) bool {
   114  	if k != imageField {
   115  		return true
   116  	}
   117  
   118  	image, ok := v.(string)
   119  	if !ok {
   120  		return true
   121  	}
   122  	parsed, err := docker.ParseReference(image)
   123  	if err != nil {
   124  		log.Entry(context.TODO()).Debugf("Couldn't parse image [%s]: %s", image, err.Error())
   125  		return false
   126  	}
   127  
   128  	is.Images = append(is.Images, graph.Artifact{
   129  		Tag:       image,
   130  		ImageName: parsed.BaseName,
   131  	})
   132  	return false
   133  }
   134  
   135  // ReplaceImages replaces image names in a list of manifests.
   136  // It doesn't replace images that are referenced by digest.
   137  func (l *ManifestList) ReplaceImages(ctx context.Context, builds []graph.Artifact, rs ResourceSelector) (ManifestList, error) {
   138  	return l.replaceImages(ctx, builds, selectLocalManifestImages, rs)
   139  }
   140  
   141  // ReplaceRemoteManifestImages replaces all image names in a list containing remote manifests.
   142  // This will even override images referenced by digest or with a different repository
   143  func (l *ManifestList) ReplaceRemoteManifestImages(ctx context.Context, builds []graph.Artifact, rs ResourceSelector) (ManifestList, error) {
   144  	return l.replaceImages(ctx, builds, selectRemoteManifestImages, rs)
   145  }
   146  
   147  func (l *ManifestList) replaceImages(ctx context.Context, builds []graph.Artifact, selector imageSelector, rs ResourceSelector) (ManifestList, error) {
   148  	_, endTrace := instrumentation.StartTrace(ctx, "ReplaceImages", map[string]string{
   149  		"manifestEntries":   strconv.Itoa(len(*l)),
   150  		"numImagesReplaced": strconv.Itoa(len(builds)),
   151  	})
   152  	defer endTrace()
   153  
   154  	replacer := newImageReplacer(builds, selector)
   155  
   156  	updated, err := l.Visit(replacer, rs)
   157  	if err != nil {
   158  		endTrace(instrumentation.TraceEndError(err))
   159  		return nil, replaceImageErr(err)
   160  	}
   161  
   162  	replacer.Check()
   163  	log.Entry(ctx).Debug("manifests with tagged images:", updated.String())
   164  
   165  	return updated, nil
   166  }
   167  
   168  type imageReplacer struct {
   169  	tagsByImageName map[string]string
   170  	found           map[string]bool
   171  	selector        imageSelector
   172  }
   173  
   174  func newImageReplacer(builds []graph.Artifact, selector imageSelector) *imageReplacer {
   175  	tagsByImageName := make(map[string]string)
   176  	for _, build := range builds {
   177  		imageName := docker.SanitizeImageName(build.ImageName)
   178  		tagsByImageName[imageName] = build.Tag
   179  	}
   180  
   181  	return &imageReplacer{
   182  		tagsByImageName: tagsByImageName,
   183  		found:           make(map[string]bool),
   184  		selector:        selector,
   185  	}
   186  }
   187  
   188  func (r *imageReplacer) Visit(gk apimachinery.GroupKind, navpath string, o map[string]interface{}, k string, v interface{}, rs ResourceSelector) bool {
   189  	if _, ok := rs.allowByNavpath(gk, navpath, k); !ok {
   190  		return true
   191  	}
   192  
   193  	image, ok := v.(string)
   194  	if !ok {
   195  		return true
   196  	}
   197  
   198  	parsed, err := docker.ParseReference(image)
   199  	if err != nil {
   200  		log.Entry(context.TODO()).Debugf("Couldn't parse image [%s]: %s", image, err.Error())
   201  		return false
   202  	}
   203  	if imageName, tag, selected := r.selector(r.tagsByImageName, parsed); selected {
   204  		r.found[imageName] = true
   205  		o[k] = tag
   206  	}
   207  	return false
   208  }
   209  
   210  func (r *imageReplacer) Check() {
   211  	for imageName := range r.tagsByImageName {
   212  		if !r.found[imageName] {
   213  			log.Entry(context.TODO()).Debugf("image [%s] is not used by the current deployment", imageName)
   214  		}
   215  	}
   216  }
   217  
   218  // imageSelector represents a strategy for matching the container `image` defined in a kubernetes manifest with the correct skaffold artifact.
   219  type imageSelector func(tagsByImageName map[string]string, image *docker.ImageReference) (imageName, tag string, valid bool)
   220  
   221  func selectLocalManifestImages(tagsByImageName map[string]string, image *docker.ImageReference) (string, string, bool) {
   222  	// Leave images referenced by digest as they are
   223  	if image.Digest != "" {
   224  		return "", "", false
   225  	}
   226  	// local manifest mentions artifact `imageName` directly, so `imageName` is parsed into `image.BaseName`
   227  	tag, present := tagsByImageName[image.BaseName]
   228  	return image.BaseName, tag, present
   229  }
   230  
   231  func selectRemoteManifestImages(tagsByImageName map[string]string, image *docker.ImageReference) (string, string, bool) {
   232  	// if manifest mentions `imageName` directly then `imageName` is parsed into `image.BaseName`
   233  	if tag, present := tagsByImageName[image.BaseName]; present {
   234  		return image.BaseName, tag, present
   235  	}
   236  	// if manifest mentions image with repository then `imageName` is parsed into `image.Name`
   237  	tag, present := tagsByImageName[image.Name]
   238  	return image.Name, tag, present
   239  }