Ctrl K
ComunidadGuías…

builder/build/dockerfile_build.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// RAINBOND, Application Management Platform
// Copyright (C) 2014-2017 Goodrain Co., Ltd.

// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version. For any non-GPL usage of Rainbond,
// one or multiple Commercial Licenses authorized by Goodrain Co., Ltd.
// must be obtained first.

// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.

// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

package build

import (
	"context"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"os"
	"path"
	"strings"
	"time"

	"github.com/eapache/channels"
	"github.com/goodrain/rainbond/builder"
	jobc "github.com/goodrain/rainbond/builder/job"
	"github.com/goodrain/rainbond/builder/sources"
	"github.com/goodrain/rainbond/db"
	"github.com/goodrain/rainbond/util"
	"github.com/sirupsen/logrus"
	corev1 "k8s.io/api/core/v1"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func dockerfileBuilder() (Build, error) {
	return &dockerfileBuild{}, nil
}

type dockerfileBuild struct {
}

func (d *dockerfileBuild) Build(re *Request) (*Response, error) {
	// 如果指定了 dockerfile_path,使用它;否则使用默认的 Dockerfile
	dockerfilePath := "Dockerfile"
	if re.CodeSouceInfo.DockerfilePath != "" {
		dockerfilePath = re.CodeSouceInfo.DockerfilePath
	}
	filepath := path.Join(re.SourceDir, dockerfilePath)
	re.Logger.Info("Start parse Dockerfile", map[string]string{"step": "builder-exector", "dockerfile_path": dockerfilePath})
	_, err := sources.ParseFile(filepath)
	if err != nil {
		logrus.Error("parse dockerfile error.", err.Error())
		re.Logger.Error(fmt.Sprintf("%s: %s", util.Translation("Parse dockerfile error"), err.Error()), map[string]string{"step": "builder-exector", "status": "failure"})
		return nil, err
	}
	buildImageName := CreateImageName(re.ServiceID, re.DeployVersion)
	if err := d.stopPreBuildJob(re); err != nil {
		logrus.Errorf("stop pre build job for service %s failure %s", re.ServiceID, err.Error())
	}
	if err := d.runBuildJob(re, buildImageName); err != nil {
		re.Logger.Error(util.Translation("Compiling the source code failure"), map[string]string{"step": "build-code", "status": "failure"})
		logrus.Error("build dockerfile job error,", err.Error())
		return nil, err
	}
	re.Logger.Info("code build success", map[string]string{"step": "build-exector"})
	return &Response{
		MediumPath: buildImageName,
		MediumType: ImageMediumType,
	}, nil
}

// The same component retains only one build task to perform
func (d *dockerfileBuild) stopPreBuildJob(re *Request) error {
	jobList, err := jobc.GetJobController().GetServiceJobs(re.ServiceID)
	if err != nil {
		logrus.Errorf("get pre build job for service %s failure ,%s", re.ServiceID, err.Error())
	}
	if len(jobList) > 0 {
		for _, job := range jobList {
			jobc.GetJobController().DeleteJob(job.Name)
		}
	}
	return nil
}

// Use buildkit to create a job to build an image
func (d *dockerfileBuild) runBuildJob(re *Request, buildImageName string) error {
	name := fmt.Sprintf("%s-%s", re.ServiceID, re.DeployVersion)
	namespace := re.RbdNamespace
	// 获取 dockerfile 路径
	dockerfilePath := "Dockerfile"
	if re.CodeSouceInfo.DockerfilePath != "" {
		dockerfilePath = re.CodeSouceInfo.DockerfilePath
	}
	job := corev1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:      name,
			Namespace: namespace,
			Labels: map[string]string{
				"service": re.ServiceID,
				"job":     "codebuild",
			},
		},
	}
	logrus.Debugf("dockerfile builder using hostpath, schedule job into current node")
	podSpec := corev1.PodSpec{
		RestartPolicy: corev1.RestartPolicyOnFailure,
		Affinity: &corev1.Affinity{
			NodeAffinity: &corev1.NodeAffinity{
				RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
					NodeSelectorTerms: []corev1.NodeSelectorTerm{{
						MatchExpressions: []corev1.NodeSelectorRequirement{
							{
								Key:      "kubernetes.io/arch",
								Operator: corev1.NodeSelectorOpIn,
								Values:   []string{re.Arch},
							},
							{
								Key:      "kubernetes.io/hostname",
								Operator: corev1.NodeSelectorOpIn,
								Values:   []string{os.Getenv("HOST_IP")},
							},
						},
					},
					},
				},
			},
		},
	}

	// only support never and onfailure
	hostIP := os.Getenv("HOST_IP")
	if hostIP != "" {
		podSpec.NodeSelector = map[string]string{
			"kubernetes.io/hostname": hostIP,
		}
		podSpec.Tolerations = []corev1.Toleration{
			{
				Operator: "Exists",
			},
		}
	}
	secret, err := d.createAuthSecret(re)
	if err != nil {
		return err
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	imageDomain, buildKitTomlCMName := sources.GetImageFirstPart(builder.REGISTRYDOMAIN)
	err = sources.PrepareBuildKitTomlCM(ctx, re.KubeClient, re.RbdNamespace, buildKitTomlCMName, imageDomain)
	if err != nil {
		return err
	}
	volumes, mounts := d.createVolumeAndMount(secret.Name, buildKitTomlCMName)
	podSpec.Volumes = volumes
	privileged := true
	container := corev1.Container{
		Name:      name,
		Image:     re.BuildKitImage,
		Stdin:     true,
		StdinOnce: true,
		Command:   []string{"buildctl-daemonless.sh"},
		Env: []corev1.EnvVar{
			{
				Name:  "BUILDCTL_CONNECT_RETRIES_MAX",
				Value: util.GetenvDefault("BUILDCTL_CONNECT_RETRIES_MAX", "100"),
			},
			{
				Name:  "BUILDKITD_FLAGS",
				Value: util.GetenvDefault("BUILDKITD_FLAGS", "--oci-worker-gc=false"),
			},
		},
		Args: []string{
			"build",
			"--frontend",
			"dockerfile.v0",
			"--local",
			fmt.Sprintf("context=%v", re.SourceDir),
			"--local",
			fmt.Sprintf("dockerfile=%v", re.SourceDir),
			"--output",
			fmt.Sprintf("type=image,name=%s,push=true", buildImageName),
		},
		SecurityContext: &corev1.SecurityContext{
			Privileged: &privileged,
		},
	}
	container.Env = append(container.Env, buildKitProxyEnvVars()...)
	// 启用基于本地目录的 layer cache,使 apt-get update 等耗时层在同一组件的
	// 重复构建中得到复用。/cache 已通过 createVolumeAndMount 挂载到宿主机的
	// /opt/rainbond/cache,按 serviceID 区分缓存目录,默认开启无需开关。
	container.Args = append(container.Args, localBuildKitCacheArgs(re.ServiceID)...)

	// 如果指定了自定义的 dockerfile 路径,添加 filename 参数
	if dockerfilePath != "Dockerfile" {
		container.Args = append(container.Args, fmt.Sprintf("--opt=filename=%s", dockerfilePath))
	}
	if len(re.BuildKitArgs) > 0 {
		container.Args = append(container.Args, re.BuildKitArgs...)
	}
	container.Args = append(container.Args, dockerfileBuildArgs(re.BuildEnvs)...)

	// 添加 BuildKit 缓存支持
	if re.BuildKitCache {
		// 使用 Registry cache,支持多阶段构建和完整的层缓存
		// 缓存镜像基于服务ID,不包含版本号,这样所有版本可以共享缓存
		baseImageName := CreateImageName(re.ServiceID, "cache")
		cacheRef := util.GetenvDefault("BUILDKIT_CACHE_REF", baseImageName)
		container.Args = append(container.Args,
			"--export-cache", fmt.Sprintf("type=registry,ref=%s,mode=max", cacheRef),
			"--import-cache", fmt.Sprintf("type=registry,ref=%s", cacheRef))
		re.Logger.Info(fmt.Sprintf("BuildKit cache enabled, cache ref: %s", cacheRef), map[string]string{"step": "builder-exector"})
	}

	container.VolumeMounts = mounts
	podSpec.Containers = append(podSpec.Containers, container)
	podSpec.HostAliases = append(podSpec.HostAliases, NormalizeHostAliases(re.HostAlias)...)
	job.Spec = podSpec
	writer := re.Logger.GetWriter("builder", "info")
	reChan := channels.NewRingChannel(10)

	logrus.Debugf("create job[name: %s; namespace: %s]", job.Name, job.Namespace)
	err = jobc.GetJobController().ExecJob(ctx, &job, writer, reChan)
	if err != nil {
		logrus.Errorf("create new job:%s failed: %s", name, err.Error())
		re.Logger.Error(util.Translation("Create build job failed"), map[string]string{"step": "builder-exector", "status": "failure"})
		return err
	}
	re.Logger.Info(util.Translation("create build code job success"), map[string]string{"step": "build-exector"})
	// delete job after complete
	defer d.deleteAuthSecret(re, secret.Name)
	defer jobc.GetJobController().DeleteJob(job.Name)
	return d.waitingComplete(re, reChan)
}

// localBuildKitCacheArgs 返回 buildctl 使用本地目录作为 layer cache 的参数。
// 缓存目录位于已挂载的 /cache 下,按 serviceID 区分,保证同一组件的重复构建
// 复用 apt-get update 等耗时层。mode=max 导出全部中间层缓存。
func localBuildKitCacheArgs(serviceID string) []string {
	cacheDir := fmt.Sprintf("/cache/buildkit/%s", serviceID)
	return []string{
		"--import-cache", fmt.Sprintf("type=local,src=%s", cacheDir),
		"--export-cache", fmt.Sprintf("type=local,dest=%s,mode=max", cacheDir),
	}
}

var buildKitProxyEnvNames = []string{
	"HTTP_PROXY",
	"HTTPS_PROXY",
	"NO_PROXY",
}

func buildKitProxyEnvVars() []corev1.EnvVar {
	envs := make([]corev1.EnvVar, 0, len(buildKitProxyEnvNames))
	for _, name := range buildKitProxyEnvNames {
		if value := os.Getenv(name); value != "" {
			envs = append(envs, corev1.EnvVar{Name: name, Value: value})
		}
	}
	return envs
}

func (d *dockerfileBuild) createVolumeAndMount(secretName string, buildKitTomlCMName string) (volumes []corev1.Volume, volumeMounts []corev1.VolumeMount) {
	hostPathType := corev1.HostPathDirectoryOrCreate
	hostsFilePathType := corev1.HostPathFile
	volumes = []corev1.Volume{
		{
			Name: "dockerfile-build",
			VolumeSource: corev1.VolumeSource{
				HostPath: &corev1.HostPathVolumeSource{
					Path: "/opt/rainbond/cache",
					Type: &hostPathType,
				},
			},
		},
		{
			Name: "grdata",
			VolumeSource: corev1.VolumeSource{
				HostPath: &corev1.HostPathVolumeSource{
					Path: "/opt/rainbond/grdata",
					Type: &hostPathType,
				},
			},
		},
		{
			Name: "buildkittoml",
			VolumeSource: corev1.VolumeSource{
				ConfigMap: &corev1.ConfigMapVolumeSource{
					LocalObjectReference: corev1.LocalObjectReference{Name: buildKitTomlCMName},
					Items: []corev1.KeyToPath{
						{
							Key:  "buildkittoml",
							Path: "buildkitd.toml",
						},
					},
				},
			},
		},
		{
			Name: "buildkit-secret",
			VolumeSource: corev1.VolumeSource{
				Secret: &corev1.SecretVolumeSource{
					SecretName: secretName,
					Items: []corev1.KeyToPath{
						{
							Key:  ".dockerconfigjson",
							Path: "config.json",
						},
					},
				},
			},
		},
		{
			Name: "hosts",
			VolumeSource: corev1.VolumeSource{
				HostPath: &corev1.HostPathVolumeSource{
					Path: "/etc/hosts",
					Type: &hostsFilePathType,
				},
			},
		},
	}
	volumeMounts = []corev1.VolumeMount{
		{
			Name:      "grdata",
			MountPath: "/grdata",
		},
		{
			Name:      "dockerfile-build",
			MountPath: "/cache",
		},
		{
			Name:      "buildkit-secret",
			MountPath: "/root/.docker",
		},
		{
			Name:      "hosts",
			MountPath: "/etc/hosts",
		},
		{
			Name:      "buildkittoml",
			MountPath: "/etc/buildkit",
		},
	}
	return volumes, volumeMounts
}

func (d *dockerfileBuild) createAuthSecret(re *Request) (sc corev1.Secret, err error) {
	var secret corev1.Secret
	tenant, err := db.GetManager().TenantDao().GetTenantByUUID(re.TenantID)
	if err != nil {
		logrus.Errorf("get tenant failed:%v", err.Error())
		re.Logger.Error(util.Translation("Get tenant info failed"), map[string]string{"step": "builder-exector", "status": "failure"})
		return secret, err
	}
	secrets, err := re.KubeClient.CoreV1().Secrets(tenant.Namespace).List(context.Background(), metav1.ListOptions{})
	if err != nil {
		logrus.Errorf("get secret failed:%v", err.Error())
		return secret, err
	}
	registryAuth := make(map[string]string)
	authOne := make(map[string]interface{})
	authAll := make(map[string]map[string]interface{})
	for _, secret = range secrets.Items {
		// Obtain the private warehouse configuration under goodrain.me
		if secret.Name == "rbd-hub-credentials" {
			configByte := secret.Data[".dockerconfigjson"]
			configStr := base64.StdEncoding.EncodeToString(configByte)
			config, err := base64.StdEncoding.DecodeString(configStr)
			if err != nil {
				logrus.Errorf("base64 decode domain error:%v", err.Error())
				continue
			}
			auths := make(map[string]interface{})
			err = json.Unmarshal(config, &auths)
			if err != nil {
				logrus.Debugf("json unmarshal config str error:%v", err.Error())
				continue
			}
			hubConfig := auths["auths"]
			if rec, ok := hubConfig.(map[string]interface{}); ok {
				for key, val := range rec {
					authOne[key] = val
					authAll["auths"] = authOne
				}
			}
		}
		// Obtain the private warehouse configuration under the team
		if strings.Contains(secret.Name, "rbd-registry-auth") {
			domainStr := base64.StdEncoding.EncodeToString(secret.Data["Domain"])
			passwordStr := base64.StdEncoding.EncodeToString(secret.Data["Password"])
			usernameStr := base64.StdEncoding.EncodeToString(secret.Data["Username"])

			domain, err := base64.StdEncoding.DecodeString(domainStr)
			if err != nil {
				logrus.Debugf("base64 decode domain error:%v", err.Error())
				continue
			}
			passWord, err := base64.StdEncoding.DecodeString(passwordStr)
			if err != nil {
				logrus.Debugf("base64 decode password error:%v", err.Error())
				continue
			}
			userName, err := base64.StdEncoding.DecodeString(usernameStr)
			if err != nil {
				logrus.Debugf("base64 decode username error:%v", err.Error())
				continue
			}
			registryAuth["username"] = string(userName)
			registryAuth["password"] = string(passWord)
			authOne[string(domain)] = registryAuth
			authAll["auths"] = authOne
		}
	}
	authByte, err := json.Marshal(authAll)
	if err != nil {
		logrus.Errorf("json unmarshal auth all error:%v", err.Error())
		return secret, err
	}
	secret = corev1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name:      fmt.Sprintf("rbd-hub-auth-%v", util.NewUUID()),
			Namespace: re.RbdNamespace,
			Labels: map[string]string{
				"creator":                          "Rainbond",
				"rainbond.io/registry-auth-secret": "true",
			},
		},
		Data: map[string][]byte{
			".dockerconfigjson": authByte,
		},
		Type: corev1.SecretTypeDockerConfigJson,
	}
	sec, err := re.KubeClient.CoreV1().Secrets(re.RbdNamespace).Create(context.Background(), &secret, metav1.CreateOptions{})
	if err != nil {
		logrus.Errorf("create secret error:%v", err.Error())
		return secret, nil
	}
	return *sec, nil
}

func (d *dockerfileBuild) deleteAuthSecret(re *Request, secretName string) {
	err := re.KubeClient.CoreV1().Secrets(re.RbdNamespace).Delete(context.Background(), secretName, metav1.DeleteOptions{})
	if err != nil {
		logrus.Errorf("delete auth secret error:%v", err.Error())
	}
}

func (d *dockerfileBuild) waitingComplete(re *Request, reChan *channels.RingChannel) (err error) {
	var logComplete = false
	var jobComplete = false
	timeout := time.NewTimer(time.Minute * 60)
	for {
		select {
		case <-timeout.C:
			return fmt.Errorf("build time out (more than 60 minute)")
		case jobStatus := <-reChan.Out():
			status := jobStatus.(string)
			switch status {
			case "complete":
				jobComplete = true
				if logComplete {
					return nil
				}
				re.Logger.Info(util.Translation("build code job exec completed"), map[string]string{"step": "build-exector"})
			case "failed":
				jobComplete = true
				err = fmt.Errorf("build code job exec failure")
				if logComplete {
					return err
				}
				re.Logger.Info(util.Translation("build code job exec failed"), map[string]string{"step": "build-exector"})
			case "cancel":
				jobComplete = true
				err = fmt.Errorf("build code job is canceled")
				if logComplete {
					return err
				}
			case "logcomplete":
				logComplete = true
				if jobComplete {
					return err
				}
			}
		}
	}
}