package cnb import ( "context" "fmt" "os" "path/filepath" "github.com/goodrain/rainbond/builder/build" jobc "github.com/goodrain/rainbond/builder/job" "github.com/goodrain/rainbond/builder/parser" "github.com/goodrain/rainbond/builder/sources" "github.com/goodrain/rainbond/util" "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" ) func init() { build.RegisterCNBBuilder(NewBuilder) } // Builder implements the build.Build interface for Cloud Native Buildpacks type Builder struct { jobCtrl jobc.Controller createAuthSecret func(*build.Request) (corev1.Secret, error) deleteAuthSecret func(*build.Request, string) prepareBuildKit func(ctx context.Context, kubeClient kubernetes.Interface, namespace, cmName, imageDomain string) error createConfigMap func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, cm *corev1.ConfigMap) (*corev1.ConfigMap, error) deleteConfigMap func(ctx context.Context, kubeClient kubernetes.Interface, namespace, name string) error } // NewBuilder creates a new CNB builder func NewBuilder() (build.Build, error) { return &Builder{ jobCtrl: jobc.GetJobController(), createAuthSecret: build.CreateAuthSecret, deleteAuthSecret: build.DeleteAuthSecret, prepareBuildKit: sources.PrepareBuildKitTomlCM, createConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace string, cm *corev1.ConfigMap) (*corev1.ConfigMap, error) { return kubeClient.CoreV1().ConfigMaps(namespace).Create(ctx, cm, metav1.CreateOptions{}) }, deleteConfigMap: func(ctx context.Context, kubeClient kubernetes.Interface, namespace, name string) error { err := kubeClient.CoreV1().ConfigMaps(namespace).Delete(ctx, name, metav1.DeleteOptions{}) if k8serrors.IsNotFound(err) { return nil } return err }, }, nil } // Build executes the CNB build process func (b *Builder) Build(re *build.Request) (*build.Response, error) { re.Logger.Info("Starting CNB build", map[string]string{"step": "builder-exector"}) if re.ServerType == "pkg" { workspace, cleanup, err := parser.PreparePackageBuildWorkspace(re.SourceDir, re.Commit.Message) if err != nil { re.Logger.Error(err.Error(), map[string]string{"step": "build-code", "status": "failure"}) return nil, err } defer cleanup() request := *re request.SourceDir = workspace re = &request re.Logger.Info("Prepared source package build input", map[string]string{"step": "build-code"}) } if err := applyVersionPolicy(re); err != nil { re.Logger.Error(err.Error(), map[string]string{"step": "build-code", "status": "failure"}) return nil, err } if err := validateSupportedBuildParams(re); err != nil { re.Logger.Error(err.Error(), map[string]string{"step": "build-code", "status": "failure"}) return nil, err } b.stopPreBuildJob(re) if err := b.validateProjectFiles(re); err != nil { return nil, err } lang := getLanguageConfig(re) if err := lang.InjectMirrorConfig(re); err != nil { logrus.Warnf("inject mirror config failed: %v", err) } if err := b.setSourceDirPermissions(re); err != nil { logrus.Warnf("set source dir permissions failed: %v", err) } buildImageName := build.CreateImageName(re.ServiceID, re.DeployVersion) if err := b.runCNBBuildJob(re, buildImageName); err != nil { re.Logger.Error(util.Translation("CNB build failed"), map[string]string{"step": "build-code", "status": "failure"}) logrus.Error("CNB build job error:", err.Error()) return nil, err } re.Logger.Info("CNB build success", map[string]string{"step": "build-exector"}) return &build.Response{ MediumPath: buildImageName, MediumType: build.ImageMediumType, }, nil } // stopPreBuildJob stops any previous build jobs for the service (best-effort). func (b *Builder) stopPreBuildJob(re *build.Request) { jobList, err := b.jobCtrl.GetServiceJobs(re.ServiceID) if err != nil { logrus.Errorf("get pre build job for service %s failure: %s", re.ServiceID, err.Error()) return } for _, job := range jobList { b.jobCtrl.DeleteJob(job.Name) } } // validateProjectFiles checks for common project configuration issues func (b *Builder) validateProjectFiles(re *build.Request) error { lockFiles := []string{"package-lock.json", "yarn.lock", "pnpm-lock.yaml"} for _, lockFile := range lockFiles { lockfilePath := filepath.Join(re.SourceDir, lockFile) if info, err := os.Stat(lockfilePath); err == nil { if info.Size() == 0 { msg := fmt.Sprintf("ERROR: %s is empty. Please run the appropriate install command (npm install / yarn install / pnpm install) to generate a valid lock file, then commit and push.", lockFile) re.Logger.Error(msg, map[string]string{"step": "build-code"}) return fmt.Errorf("empty lock file: %s", lockFile) } } } return nil } // setSourceDirPermissions removes .git and sets permissions for CNB build func (b *Builder) setSourceDirPermissions(re *build.Request) error { if !truthyBuildEnv(firstNonEmptyEnv(re.BuildEnvs, "KEEP_GIT", "BUILD_KEEP_GIT")) { gitDir := filepath.Join(re.SourceDir, ".git") if _, err := os.Stat(gitDir); err == nil { if err := os.RemoveAll(gitDir); err != nil { logrus.Warnf("failed to remove .git directory: %v", err) } } } // chmod 777 so files are accessible; actual chown to cnb user // is done inside the CNB Job container where the cnb user exists return filepath.WalkDir(re.SourceDir, func(path string, d os.DirEntry, err error) error { if err != nil { return err } return os.Chmod(path, 0777) }) }