github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/cluster/kaniko.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 cluster
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  
    25  	v1 "k8s.io/api/core/v1"
    26  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    27  	corev1 "k8s.io/client-go/kubernetes/typed/core/v1"
    28  
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/build/kaniko"
    30  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/docker"
    31  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes"
    32  	kubernetesclient "github.com/GoogleContainerTools/skaffold/pkg/skaffold/kubernetes/client"
    33  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    34  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/platform"
    35  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    36  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/util"
    37  )
    38  
    39  const initContainer = "kaniko-init-container"
    40  
    41  func (b *Builder) buildWithKaniko(ctx context.Context, out io.Writer, workspace string, artifactName string, artifact *latest.KanikoArtifact, tag string, requiredImages map[string]*string, platforms platform.Matcher) (string, error) {
    42  	// TODO: Implement building multi-platform images for cluster builder
    43  	if platforms.IsMultiPlatform() {
    44  		log.Entry(ctx).Warnf("multiple target platforms %q found for artifact %q. Skaffold doesn't yet support multi-platform builds for the docker builder. Consider specifying a single target platform explicitly. See https://skaffold.dev/docs/pipeline-stages/builders/#cross-platform-build-support", platforms.String(), artifactName)
    45  	}
    46  
    47  	generatedEnvs, err := generateEnvFromImage(tag)
    48  	if err != nil {
    49  		return "", fmt.Errorf("error processing generated env variables from image uri: %w", err)
    50  	}
    51  	env, err := evaluateEnv(artifact.Env, generatedEnvs...)
    52  	if err != nil {
    53  		return "", fmt.Errorf("unable to evaluate env variables: %w", err)
    54  	}
    55  	artifact.Env = env
    56  
    57  	buildArgs, err := docker.EvalBuildArgsWithEnv(b.cfg.Mode(), workspace, artifact.DockerfilePath, artifact.BuildArgs, requiredImages, envMapFromVars(artifact.Env))
    58  	if err != nil {
    59  		return "", fmt.Errorf("unable to evaluate build args: %w", err)
    60  	}
    61  	artifact.BuildArgs = buildArgs
    62  
    63  	client, err := kubernetesclient.DefaultClient()
    64  	if err != nil {
    65  		return "", fmt.Errorf("getting Kubernetes client: %w", err)
    66  	}
    67  	pods := client.CoreV1().Pods(b.Namespace)
    68  
    69  	podSpec, err := b.kanikoPodSpec(artifact, tag, platforms)
    70  	if err != nil {
    71  		return "", err
    72  	}
    73  
    74  	pod, err := pods.Create(ctx, podSpec, metav1.CreateOptions{})
    75  	if err != nil {
    76  		return "", fmt.Errorf("creating kaniko pod: %w", err)
    77  	}
    78  	defer func() {
    79  		if err := pods.Delete(ctx, pod.Name, metav1.DeleteOptions{
    80  			GracePeriodSeconds: new(int64),
    81  		}); err != nil {
    82  			log.Entry(ctx).Fatalf("deleting pod: %s", err)
    83  		}
    84  	}()
    85  
    86  	if err := b.copyKanikoBuildContext(ctx, workspace, artifactName, artifact, pods, pod.Name); err != nil {
    87  		return "", fmt.Errorf("copying sources: %w", err)
    88  	}
    89  
    90  	// Wait for the pods to succeed while streaming the logs
    91  	waitForLogs := streamLogs(ctx, out, pod.Name, pods)
    92  
    93  	if err := kubernetes.WaitForPodSucceeded(ctx, pods, pod.Name, b.timeout); err != nil {
    94  		waitForLogs()
    95  		return "", err
    96  	}
    97  
    98  	waitForLogs()
    99  
   100  	return docker.RemoteDigest(tag, b.cfg, nil)
   101  }
   102  
   103  // first copy over the buildcontext tarball into the init container tmp dir via kubectl cp
   104  // Via kubectl exec, we extract the tarball to the empty dir
   105  // Then, via kubectl exec, create the /tmp/complete file via kubectl exec to complete the init container
   106  func (b *Builder) copyKanikoBuildContext(ctx context.Context, workspace string, artifactName string, artifact *latest.KanikoArtifact, pods corev1.PodInterface, podName string) error {
   107  	if err := kubernetes.WaitForPodInitialized(ctx, pods, podName); err != nil {
   108  		return fmt.Errorf("waiting for pod to initialize: %w", err)
   109  	}
   110  
   111  	errs := make(chan error, 1)
   112  	buildCtx, buildCtxWriter := io.Pipe()
   113  	go func() {
   114  		err := docker.CreateDockerTarContext(ctx, buildCtxWriter, docker.NewBuildConfig(
   115  			workspace, artifactName, artifact.DockerfilePath, artifact.BuildArgs), b.cfg)
   116  		if err != nil {
   117  			buildCtxWriter.CloseWithError(fmt.Errorf("creating docker context: %w", err))
   118  			errs <- err
   119  			return
   120  		}
   121  		buildCtxWriter.Close()
   122  	}()
   123  
   124  	// Send context by piping into `tar`.
   125  	// In case of an error, print the command's output. (The `err` itself is useless: exit status 1).
   126  	var out bytes.Buffer
   127  	if err := b.kubectlcli.Run(ctx, buildCtx, &out, "exec", "-i", podName, "-c", initContainer, "-n", b.Namespace, "--", "tar", "-xf", "-", "-C", kaniko.DefaultEmptyDirMountPath); err != nil {
   128  		errRun := fmt.Errorf("uploading build context: %s", out.String())
   129  		errTar := <-errs
   130  		if errTar != nil {
   131  			errRun = fmt.Errorf("%v\ntar errors: %w", errRun, errTar)
   132  		}
   133  		return errRun
   134  	}
   135  
   136  	// Generate a file to successfully terminate the init container.
   137  	if out, err := b.kubectlcli.RunOut(ctx, "exec", podName, "-c", initContainer, "-n", b.Namespace, "--", "touch", "/tmp/complete"); err != nil {
   138  		return fmt.Errorf("finishing upload of the build context: %s", out)
   139  	}
   140  
   141  	return nil
   142  }
   143  
   144  func evaluateEnv(env []v1.EnvVar, additional ...v1.EnvVar) ([]v1.EnvVar, error) {
   145  	// Prepare additional envs
   146  	addEnv := make(map[string]string)
   147  	for _, addEnvVar := range additional {
   148  		addEnv[addEnvVar.Name] = addEnvVar.Value
   149  	}
   150  
   151  	// Evaluate provided env variables
   152  	var evaluated []v1.EnvVar
   153  	for _, envVar := range env {
   154  		val, err := util.ExpandEnvTemplate(envVar.Value, nil)
   155  		if err != nil {
   156  			return nil, fmt.Errorf("unable to get value for env variable %q: %w", envVar.Name, err)
   157  		}
   158  
   159  		evaluated = append(evaluated, v1.EnvVar{Name: envVar.Name, Value: val})
   160  
   161  		// Provided env variables have higher priority than additional (generated) ones
   162  		delete(addEnv, envVar.Name)
   163  	}
   164  
   165  	// Append additional (generated) env variables
   166  	for name, value := range addEnv {
   167  		if value != "" {
   168  			evaluated = append(evaluated, v1.EnvVar{Name: name, Value: value})
   169  		}
   170  	}
   171  
   172  	return evaluated, nil
   173  }
   174  
   175  func envMapFromVars(env []v1.EnvVar) map[string]string {
   176  	envMap := make(map[string]string)
   177  	for _, envVar := range env {
   178  		envMap[envVar.Name] = envVar.Value
   179  	}
   180  	return envMap
   181  }
   182  
   183  func generateEnvFromImage(imageStr string) ([]v1.EnvVar, error) {
   184  	imgRef, err := docker.ParseReference(imageStr)
   185  	if err != nil {
   186  		return nil, err
   187  	}
   188  	if imgRef.Tag == "" {
   189  		imgRef.Tag = "latest"
   190  	}
   191  	var generatedEnvs []v1.EnvVar
   192  	generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_REPO", Value: imgRef.Repo})
   193  	generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_NAME", Value: imgRef.Name})
   194  	generatedEnvs = append(generatedEnvs, v1.EnvVar{Name: "IMAGE_TAG", Value: imgRef.Tag})
   195  	return generatedEnvs, nil
   196  }