builder/build/cnb/job.go
Rama main · réplica de lectura del repositorio original.
package cnb
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/eapache/channels"
"github.com/goodrain/rainbond/builder"
"github.com/goodrain/rainbond/builder/build"
"github.com/goodrain/rainbond/builder/parser/code"
"github.com/goodrain/rainbond/builder/sources"
"github.com/goodrain/rainbond/util"
"github.com/sirupsen/logrus"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// runCNBBuildJob creates and runs the CNB build pod
func (b *Builder) runCNBBuildJob(re *build.Request, buildImageName string) error {
name := fmt.Sprintf("%s-%s", re.ServiceID, re.DeployVersion)
namespace := re.RbdNamespace
cnbBuilderImage := GetCNBBuilderImageForLanguage(re.Lang)
cnbRunImage := GetCNBRunImageForLanguage(re.Lang)
bindings, err := b.resolvePlatformBindings(re)
if err != nil {
return err
}
podSpec := corev1.PodSpec{
RestartPolicy: corev1.RestartPolicyNever,
}
// Set node affinity for architecture and hostname
nodeSelectors := []corev1.NodeSelectorRequirement{
{
Key: "kubernetes.io/arch",
Operator: corev1.NodeSelectorOpIn,
Values: []string{re.Arch},
},
}
if hostIP := os.Getenv("HOST_IP"); hostIP != "" {
nodeSelectors = append(nodeSelectors, corev1.NodeSelectorRequirement{
Key: "kubernetes.io/hostname",
Operator: corev1.NodeSelectorOpIn,
Values: []string{hostIP},
})
}
podSpec.Affinity = &corev1.Affinity{
NodeAffinity: &corev1.NodeAffinity{
RequiredDuringSchedulingIgnoredDuringExecution: &corev1.NodeSelector{
NodeSelectorTerms: []corev1.NodeSelectorTerm{{
MatchExpressions: nodeSelectors,
}},
},
},
}
podSpec.Tolerations = []corev1.Toleration{{Operator: "Exists"}}
rootUser := int64(0)
podSpec.SecurityContext = &corev1.PodSecurityContext{
RunAsUser: &rootUser,
RunAsGroup: &rootUser,
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
secret, err := b.createAuthSecret(re)
if err != nil {
return err
}
defer b.deleteAuthSecret(re, secret.Name)
imageDomain, buildKitTomlCMName := sources.GetImageFirstPart(builder.REGISTRYDOMAIN)
err = b.prepareBuildKit(ctx, re.KubeClient, re.RbdNamespace, buildKitTomlCMName, imageDomain)
if err != nil {
return err
}
procfileBinding, cleanupProcfileBinding, err := b.createProcfileBinding(ctx, re, name)
if err != nil {
return err
}
if cleanupProcfileBinding != nil {
defer cleanupProcfileBinding()
}
if procfileBinding != nil {
bindings = append(bindings, *procfileBinding)
}
// Compute platform annotations after bindings so derived BP_* values are included.
annotations := b.buildPlatformAnnotations(re)
for _, binding := range bindings {
annotations[bindingTypeAnnotationKey(binding.Name)] = binding.Type
}
job := corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
Labels: map[string]string{
"service": re.ServiceID,
"job": "codebuild",
"build-type": "cnb",
},
Annotations: annotations,
},
}
volumes, mounts := b.createVolumeAndMount(re, secret.Name)
platformVolume, platformMount := b.createPlatformVolume(annotations, bindings)
if platformVolume != nil {
volumes = append(volumes, *platformVolume)
mounts = append(mounts, *platformMount)
}
podSpec.Volumes = volumes
creatorArgs := b.buildCreatorArgs(re, buildImageName, cnbRunImage)
// Chown workspace to cnb user inside the builder image (where cnb user exists with correct UID),
// then exec the lifecycle creator. This ensures buildpacks can chmod generated files.
container := corev1.Container{
Name: name,
Image: cnbBuilderImage,
ImagePullPolicy: corev1.PullIfNotPresent,
Stdin: true,
StdinOnce: true,
Command: []string{"sh", "-c", `chown -R cnb:cnb /workspace && exec "$@"`, "--"},
Args: append([]string{CNBLifecycleCreatorPath}, creatorArgs...),
Env: b.buildEnvVars(re),
VolumeMounts: mounts,
}
podSpec.Containers = append(podSpec.Containers, container)
podSpec.HostAliases = append(podSpec.HostAliases, build.NormalizeHostAliases(re.HostAlias)...)
job.Spec = podSpec
writer := newCNBPhaseWriter(re.Logger.GetWriter("builder", "info"), re.Logger)
defer writer.finish()
reChan := channels.NewRingChannel(10)
logrus.Debugf("create CNB job[name: %s; namespace: %s]", job.Name, job.Namespace)
err = b.jobCtrl.ExecJob(ctx, &job, writer, reChan)
if err != nil {
logrus.Errorf("create CNB job %s failed: %s", name, err.Error())
re.Logger.Error(util.Translation("Create CNB build job failed"), map[string]string{"step": "builder-exector", "status": "failure"})
return err
}
re.Logger.Info(util.Translation("create CNB build job success"), map[string]string{"step": "build-exector"})
defer b.jobCtrl.DeleteJob(job.Name)
err = b.waitingComplete(re, reChan)
writer.finish()
return err
}
// buildCreatorArgs builds the lifecycle creator arguments
func (b *Builder) buildCreatorArgs(re *build.Request, buildImageName, runImage string) []string {
registryHost, _ := sources.GetImageFirstPart(builder.REGISTRYDOMAIN)
latestImage := stableImageTag(buildImageName, "latest")
logLevel := "info"
if v := re.BuildEnvs["CNB_LOG_LEVEL"]; v != "" {
logLevel = v
}
args := []string{
"-app=/workspace",
"-layers=/layers",
"-platform=/platform",
"-run-image=" + runImage,
"-tag=" + latestImage,
"-insecure-registry=" + registryHost,
"-log-level=" + logLevel,
}
policy := resolveCNBCachePolicy(re)
policy.log(re)
if !policy.reuseImage {
args = append(args, "-skip-restore")
} else {
args = append(args, "-previous-image="+latestImage)
if policy.registryCache {
args = append(args, "-cache-image="+stableImageTag(buildImageName, "cnb-cache"), "-parallel")
}
}
// Custom order from language config (e.g., pure static projects need nginx-only order)
lang := getLanguageConfig(re)
if bps := lang.CustomOrder(re); len(bps) > 0 {
if flag := b.writeCustomOrder(re, bps, "custom language order"); flag != "" {
args = append(args, flag)
}
}
args = append(args, buildImageName)
return args
}
// buildEnvVars builds environment variables for the CNB build container.
func (b *Builder) buildEnvVars(re *build.Request) []corev1.EnvVar {
registryHost, _ := sources.GetImageFirstPart(builder.REGISTRYDOMAIN)
langLocale := resolveBuildLocale(re, "LANG", "C.UTF-8")
lcAllLocale := resolveBuildLocale(re, "LC_ALL", langLocale)
envs := []corev1.EnvVar{
{Name: "CNB_PLATFORM_API", Value: "0.13"},
{Name: "CNB_INSECURE_REGISTRIES", Value: registryHost},
{Name: "DOCKER_CONFIG", Value: "/home/cnb/.docker"},
// Keep lifecycle and buildpack tools on UTF-8 so non-ASCII resource names survive packaging.
{Name: "LANG", Value: langLocale},
{Name: "LC_ALL", Value: lcAllLocale},
}
if httpProxy := os.Getenv("HTTP_PROXY"); httpProxy != "" {
envs = append(envs, corev1.EnvVar{Name: "HTTP_PROXY", Value: httpProxy})
}
if httpsProxy := os.Getenv("HTTPS_PROXY"); httpsProxy != "" {
envs = append(envs, corev1.EnvVar{Name: "HTTPS_PROXY", Value: httpsProxy})
}
if noProxy := os.Getenv("NO_PROXY"); noProxy != "" {
envs = append(envs, corev1.EnvVar{Name: "NO_PROXY", Value: noProxy})
}
lang := getLanguageConfig(re)
envs = append(envs, lang.BuildEnvVars(re)...)
return envs
}
func (b *Builder) resolvePlatformBindings(re *build.Request) ([]platformBinding, error) {
ctx := re.Ctx
if ctx == nil {
ctx = context.Background()
}
switch re.Lang {
case code.JavaMaven:
explicitName := strings.TrimSpace(firstNonEmptyEnv(re.BuildEnvs, "BUILD_MAVEN_SETTING_NAME", "MAVEN_SETTING_NAME"))
configMapName := ""
if explicitName != "" {
configMapName = b.jobCtrl.GetLanguageBuildSetting(ctx, code.JavaMaven, explicitName)
if configMapName == "" {
return nil, fmt.Errorf("maven setting config %s not found", explicitName)
}
} else {
configMapName = b.jobCtrl.GetDefaultLanguageBuildSetting(ctx, code.JavaMaven)
}
if configMapName == "" {
return nil, nil
}
re.BuildEnvs["BP_MAVEN_SETTINGS_PATH"] = fmt.Sprintf("/platform/bindings/%s/settings.xml", configMapName)
return []platformBinding{{
Name: configMapName,
Type: "maven",
ConfigMapName: configMapName,
ConfigMapKey: "mavensetting",
TargetFile: "settings.xml",
}}, nil
case code.NetCore:
configMapName := strings.TrimSpace(firstNonEmptyEnv(re.BuildEnvs, "BUILD_NUGET_CONFIG_NAME"))
if configMapName == "" {
return nil, nil
}
if resolved := b.jobCtrl.GetLanguageBuildSetting(ctx, code.NetCore, configMapName); resolved == "" {
return nil, fmt.Errorf("nuget config %s not found", configMapName)
}
return []platformBinding{{
Name: configMapName,
Type: "nugetconfig",
ConfigMapName: configMapName,
ConfigMapKey: "nuget.config",
TargetFile: "nuget.config",
}}, nil
default:
return nil, nil
}
}
func (b *Builder) createProcfileBinding(ctx context.Context, re *build.Request, jobName string) (*platformBinding, func(), error) {
procfile, _ := resolveProcfileBindingContent(re)
if strings.TrimSpace(procfile) == "" {
return nil, nil, nil
}
if b.createConfigMap == nil || b.deleteConfigMap == nil {
return nil, nil, fmt.Errorf("procfile binding handlers are not configured")
}
if re.KubeClient == nil && b.createConfigMap == nil {
return nil, nil, fmt.Errorf("kube client is required for procfile binding")
}
configMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
GenerateName: fmt.Sprintf("%s-procfile-", strings.ToLower(jobName)),
Namespace: re.RbdNamespace,
Labels: map[string]string{
"service": re.ServiceID,
"job": "codebuild",
"build-type": "cnb",
},
},
Data: map[string]string{
"Procfile": normalizeProcfileBindingContent(procfile),
},
}
createdConfigMap, err := b.createConfigMap(ctx, re.KubeClient, re.RbdNamespace, configMap)
if err != nil {
return nil, nil, err
}
cleanup := func() {
if err := b.deleteConfigMap(ctx, re.KubeClient, re.RbdNamespace, createdConfigMap.Name); err != nil {
logrus.Warnf("delete procfile binding configmap %s failed: %v", createdConfigMap.Name, err)
}
}
return &platformBinding{
Name: createdConfigMap.Name,
Type: "Procfile",
ConfigMapName: createdConfigMap.Name,
ConfigMapKey: "Procfile",
TargetFile: "Procfile",
}, cleanup, nil
}
// createVolumeAndMount creates volumes and volume mounts for the CNB build pod
func (b *Builder) createVolumeAndMount(re *build.Request, secretName string) ([]corev1.Volume, []corev1.VolumeMount) {
hostPathType := corev1.HostPathDirectoryOrCreate
hostPathDirectoryType := corev1.HostPathDirectory
volumes := []corev1.Volume{
{
Name: "workspace",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: filepath.Join("/opt/rainbond/", re.SourceDir),
Type: &hostPathDirectoryType,
},
},
},
{
Name: "layers",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: "grdata",
VolumeSource: corev1.VolumeSource{
HostPath: &corev1.HostPathVolumeSource{
Path: "/opt/rainbond/grdata",
Type: &hostPathType,
},
},
},
{
Name: "docker-config",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: secretName,
Items: []corev1.KeyToPath{
{Key: ".dockerconfigjson", Path: "config.json"},
},
},
},
},
}
volumeMounts := []corev1.VolumeMount{
{Name: "workspace", MountPath: "/workspace"},
{Name: "layers", MountPath: "/layers"},
{Name: "grdata", MountPath: "/grdata"},
{Name: "docker-config", MountPath: "/home/cnb/.docker"},
}
return volumes, volumeMounts
}
func resolveProcfileBindingContent(re *build.Request) (string, string) {
if procfile := re.BuildEnvs["BUILD_PROCFILE"]; strings.TrimSpace(procfile) != "" {
source := strings.TrimSpace(re.BuildEnvs["START_COMMAND_SOURCE"])
if source == "" {
source = "procfile"
}
return procfile, source
}
if procfile := re.BuildEnvs["BUILD_AUTO_PROCFILE"]; strings.TrimSpace(procfile) != "" {
source := strings.TrimSpace(re.BuildEnvs["START_COMMAND_SOURCE"])
if source == "" {
source = "auto-detected"
}
return procfile, source
}
if re.SourceDir == "" {
return "", ""
}
body, err := os.ReadFile(filepath.Join(re.SourceDir, "Procfile"))
if err != nil || strings.TrimSpace(string(body)) == "" {
return "", ""
}
return string(body), "procfile"
}
func normalizeProcfileBindingContent(procfile string) string {
procfile = strings.TrimRight(procfile, "\r\n")
return procfile + "\n"
}
// waitingComplete waits for the build job to complete
func (b *Builder) waitingComplete(re *build.Request, reChan *channels.RingChannel) error {
logComplete := false
jobComplete := false
var err error
timeout := time.NewTimer(time.Minute * 60)
defer timeout.Stop()
for {
select {
case <-timeout.C:
return fmt.Errorf("cnb build timed out (exceeded 60 minutes)")
case jobStatus := <-reChan.Out():
status, ok := jobStatus.(string)
if !ok {
logrus.Warnf("unexpected job status type: %T", jobStatus)
continue
}
switch status {
case "complete":
jobComplete = true
if logComplete {
return nil
}
re.Logger.Info(util.Translation("CNB build job completed"), map[string]string{"step": "build-exector"})
case "failed":
jobComplete = true
err = fmt.Errorf("cnb build job exec failure")
if logComplete {
return err
}
re.Logger.Info(util.Translation("CNB build job failed"), map[string]string{"step": "build-exector"})
case "cancel":
jobComplete = true
err = fmt.Errorf("cnb build job is canceled")
if logComplete {
return err
}
case "logcomplete":
logComplete = true
if jobComplete {
return err
}
}
}
}
}
// stableImageTag replaces the tag portion of an image reference.
// e.g. "goodrain.me/workload:20260220215515" + "latest" → "goodrain.me/workload:latest"
func stableImageTag(imageName, tag string) string {
if i := strings.LastIndex(imageName, ":"); i != -1 {
return imageName[:i] + ":" + tag
}
return imageName + ":" + tag
}