github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/kubernetes/loader/load.go (about)

     1  /*
     2  Copyright 2021 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 loader
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"io"
    23  	"os/exec"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/docker/distribution/reference"
    28  	"k8s.io/client-go/tools/clientcmd/api"
    29  
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/config"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    32  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubectl"
    34  	kubectx "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/context"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    37  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/stringslice"
    38  	timeutil "github.com/GoogleContainerTools/skaffold/pkg/skaffold/util/time"
    39  )
    40  
    41  type ImageLoader struct {
    42  	kubeContext string
    43  	cli         *kubectl.CLI
    44  }
    45  
    46  type Config interface {
    47  	kubectl.Config
    48  
    49  	GetKubeContext() string
    50  	LoadImages() bool
    51  }
    52  
    53  func NewImageLoader(kubeContext string, cli *kubectl.CLI) *ImageLoader {
    54  	return &ImageLoader{
    55  		kubeContext: kubeContext,
    56  		cli:         cli,
    57  	}
    58  }
    59  
    60  // We only load images that
    61  //    1) Were identified as local images by the Runner, and
    62  //    2) Are part of the set of images being deployed by a given Deployer, so we don't duplicate effort
    63  
    64  func imagesToLoad(localImages, deployerImages, images []graph.Artifact) []graph.Artifact {
    65  	local := map[string]bool{}
    66  	for _, image := range localImages {
    67  		local[docker.SanitizeImageName(image.ImageName)] = true
    68  	}
    69  
    70  	tracked := map[string]bool{}
    71  	for _, image := range deployerImages {
    72  		if local[image.ImageName] {
    73  			tracked[image.ImageName] = true
    74  		}
    75  	}
    76  
    77  	var res []graph.Artifact
    78  	for _, image := range images {
    79  		if tracked[docker.SanitizeImageName(image.ImageName)] {
    80  			res = append(res, image)
    81  		}
    82  	}
    83  	return res
    84  }
    85  
    86  // LoadImages loads images into a local cluster.
    87  // imagesToLoad is used to determine the set of images we should load, based on images that are
    88  // marked as local by the Runner, and part of the calling Deployer's set of manifests
    89  func (i *ImageLoader) LoadImages(ctx context.Context, out io.Writer, localImages, deployerImages, images []graph.Artifact) error {
    90  	currentContext, err := i.getCurrentContext()
    91  	if err != nil {
    92  		return err
    93  	}
    94  
    95  	artifacts := imagesToLoad(localImages, deployerImages, images)
    96  
    97  	if config.IsKindCluster(i.kubeContext) {
    98  		kindCluster := config.KindClusterName(currentContext.Cluster)
    99  
   100  		// With `kind`, docker images have to be loaded with the `kind` CLI.
   101  		if err := i.loadImagesInKindNodes(ctx, out, kindCluster, artifacts); err != nil {
   102  			return fmt.Errorf("loading images into kind nodes: %w", err)
   103  		}
   104  	}
   105  
   106  	if config.IsK3dCluster(i.kubeContext) {
   107  		k3dCluster := config.K3dClusterName(currentContext.Cluster)
   108  
   109  		// With `k3d`, docker images have to be loaded with the `k3d` CLI.
   110  		if err := i.loadImagesInK3dNodes(ctx, out, k3dCluster, artifacts); err != nil {
   111  			return fmt.Errorf("loading images into k3d nodes: %w", err)
   112  		}
   113  	}
   114  
   115  	return nil
   116  }
   117  
   118  // loadImagesInKindNodes loads artifact images into every node of a kind cluster.
   119  func (i *ImageLoader) loadImagesInKindNodes(ctx context.Context, out io.Writer, kindCluster string, artifacts []graph.Artifact) error {
   120  	output.Default.Fprintln(out, "Loading images into kind cluster nodes...")
   121  	return i.loadImages(ctx, out, artifacts, func(tag string) *exec.Cmd {
   122  		return exec.CommandContext(ctx, "kind", "load", "docker-image", "--name", kindCluster, tag)
   123  	})
   124  }
   125  
   126  // loadImagesInK3dNodes loads artifact images into every node of a k3s cluster.
   127  func (i *ImageLoader) loadImagesInK3dNodes(ctx context.Context, out io.Writer, k3dCluster string, artifacts []graph.Artifact) error {
   128  	output.Default.Fprintln(out, "Loading images into k3d cluster nodes...")
   129  	return i.loadImages(ctx, out, artifacts, func(tag string) *exec.Cmd {
   130  		return exec.CommandContext(ctx, "k3d", "image", "import", "--cluster", k3dCluster, tag)
   131  	})
   132  }
   133  
   134  func (i *ImageLoader) loadImages(ctx context.Context, out io.Writer, artifacts []graph.Artifact, createCmd func(tag string) *exec.Cmd) error {
   135  	start := time.Now()
   136  
   137  	var knownImages []string
   138  
   139  	for _, artifact := range artifacts {
   140  		output.Default.Fprintf(out, " - %s -> ", artifact.Tag)
   141  
   142  		// Only load images that are unknown to the node
   143  		if knownImages == nil {
   144  			var err error
   145  			if knownImages, err = findKnownImages(ctx, i.cli); err != nil {
   146  				return fmt.Errorf("unable to retrieve node's images: %w", err)
   147  			}
   148  		}
   149  		normalizedImageRef, err := reference.ParseNormalizedNamed(artifact.Tag)
   150  		if err != nil {
   151  			return err
   152  		}
   153  		if stringslice.Contains(knownImages, normalizedImageRef.String()) {
   154  			output.Green.Fprintln(out, "Found")
   155  			continue
   156  		}
   157  
   158  		cmd := createCmd(artifact.Tag)
   159  		if cmdOut, err := util.RunCmdOut(ctx, cmd); err != nil {
   160  			output.Red.Fprintln(out, "Failed")
   161  			return fmt.Errorf("unable to load image %q into cluster: %w, %s", artifact.Tag, err, cmdOut)
   162  		}
   163  
   164  		output.Green.Fprintln(out, "Loaded")
   165  	}
   166  
   167  	output.Default.Fprintln(out, "Images loaded in", timeutil.Humanize(time.Since(start)))
   168  	return nil
   169  }
   170  
   171  func findKnownImages(ctx context.Context, cli *kubectl.CLI) ([]string, error) {
   172  	nodeGetOut, err := cli.RunOut(ctx, "get", "nodes", `-ojsonpath='{@.items[*].status.images[*].names[*]}'`)
   173  	if err != nil {
   174  		return nil, fmt.Errorf("unable to inspect the nodes: %w", err)
   175  	}
   176  
   177  	knownImages := strings.Split(string(nodeGetOut), " ")
   178  	return knownImages, nil
   179  }
   180  
   181  func (i *ImageLoader) getCurrentContext() (*api.Context, error) {
   182  	currentCfg, err := kubectx.CurrentConfig()
   183  	if err != nil {
   184  		return nil, fmt.Errorf("unable to get kubernetes config: %w", err)
   185  	}
   186  
   187  	currentContext, present := currentCfg.Contexts[i.kubeContext]
   188  	if !present {
   189  		return nil, fmt.Errorf("unable to get current kubernetes context: %w", err)
   190  	}
   191  	return currentContext, nil
   192  }