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 }