github.com/projecteru2/core@v0.0.0-20240321043226-06bcc1c23f58/engine/docker/build.go (about)

     1  package docker
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"io"
     7  	"os"
     8  	"path/filepath"
     9  	"strings"
    10  
    11  	"github.com/projecteru2/core/engine/types"
    12  	enginetypes "github.com/projecteru2/core/engine/types"
    13  	"github.com/projecteru2/core/log"
    14  	coresource "github.com/projecteru2/core/source"
    15  	coretypes "github.com/projecteru2/core/types"
    16  	"github.com/projecteru2/core/utils"
    17  )
    18  
    19  const (
    20  	fromAsTmpl = "FROM %s as %s"
    21  	commonTmpl = `{{ range $k, $v:= .Args -}}
    22  {{ printf "ARG %s=%q" $k $v }}
    23  {{ end -}}
    24  {{ range $k, $v:= .Envs -}}
    25  {{ printf "ENV %s %q" $k $v }}
    26  {{ end -}}
    27  {{ range $k, $v:= .Labels -}}
    28  {{ printf "LABEL %s=%s" $k $v }}
    29  {{ end -}}
    30  {{- if .Dir}}RUN mkdir -p {{.Dir}}
    31  WORKDIR {{.Dir}}{{ end }}
    32  {{ if .Repo }}ADD {{.Repo}} .{{ end }}
    33  {{ if .StopSignal }}STOPSIGNAL {{.StopSignal}} {{ end }}`
    34  	copyTmpl = "COPY --from=%s %s %s"
    35  	runTmpl  = "RUN %s"
    36  	// TODO consider work dir privilege
    37  	// Add user manually
    38  	userTmpl = `RUN echo "{{.User}}::{{.UID}}:{{.UID}}:{{.User}}:/dev/null:/sbin/nologin" >> /etc/passwd && \
    39  echo "{{.User}}:x:{{.UID}}:" >> /etc/group && \
    40  echo "{{.User}}:!::0:::::" >> /etc/shadow
    41  USER {{.User}}
    42  `
    43  )
    44  
    45  // BuildRefs output refs
    46  func (e *Engine) BuildRefs(_ context.Context, opts *enginetypes.BuildRefOptions) []string {
    47  	name := opts.Name
    48  	tags := opts.Tags
    49  	refs := []string{}
    50  	for _, tag := range tags {
    51  		ref := createImageTag(e.config.Docker, name, tag)
    52  		refs = append(refs, ref)
    53  	}
    54  	// use latest
    55  	if len(refs) == 0 {
    56  		refs = append(refs, createImageTag(e.config.Docker, name, utils.DefaultVersion))
    57  	}
    58  	return refs
    59  }
    60  
    61  // BuildContent generate build content
    62  // since we wanna set UID for the user inside workload, we have to know the uid parameter
    63  //
    64  // build directory is like:
    65  //
    66  //	buildDir ├─ :appname ├─ code
    67  //	         ├─ Dockerfile
    68  func (e *Engine) BuildContent(ctx context.Context, scm coresource.Source, opts *enginetypes.BuildContentOptions) (string, io.Reader, error) {
    69  	if opts.Builds == nil {
    70  		return "", nil, coretypes.ErrNoBuildsInSpec
    71  	}
    72  	// make build dir
    73  	buildDir, err := os.MkdirTemp(os.TempDir(), "corebuild-")
    74  	if err != nil {
    75  		return "", nil, err
    76  	}
    77  	log.WithFunc("engine.docker.BuildContent").Debugf(ctx, "Build dir %s", buildDir)
    78  	// create dockerfile
    79  	if err := e.makeDockerFile(ctx, opts, scm, buildDir); err != nil {
    80  		return buildDir, nil, err
    81  	}
    82  	// create stream for Build API
    83  	tar, err := CreateTarStream(buildDir)
    84  	return buildDir, tar, err
    85  }
    86  
    87  func (e *Engine) makeDockerFile(ctx context.Context, opts *types.BuildContentOptions, scm coresource.Source, buildDir string) error {
    88  	var preCache map[string]string
    89  	var preStage string
    90  	var buildTmpl []string
    91  
    92  	for _, stage := range opts.Builds.Stages {
    93  		build, ok := opts.Builds.Builds[stage]
    94  		if !ok {
    95  			log.WithFunc("engine.docker.makeDockerFile").Warnf(ctx, "Builds stage %s not defined", stage)
    96  			continue
    97  		}
    98  
    99  		// get source or artifacts
   100  		reponame, err := e.preparedSource(ctx, build, scm, buildDir)
   101  		if err != nil {
   102  			return err
   103  		}
   104  		build.Repo = reponame
   105  
   106  		// get header
   107  		from := fmt.Sprintf(fromAsTmpl, build.Base, stage)
   108  
   109  		// get multiple stags
   110  		copys := []string{}
   111  		for src, dst := range preCache {
   112  			copys = append(copys, fmt.Sprintf(copyTmpl, preStage, src, dst))
   113  		}
   114  
   115  		// get commands
   116  		commands := []string{}
   117  		for _, command := range build.Commands {
   118  			commands = append(commands, fmt.Sprintf(runTmpl, command))
   119  		}
   120  
   121  		// decide add source or not
   122  		mainPart, err := makeMainPart(opts, build, from, commands, copys)
   123  		if err != nil {
   124  			return err
   125  		}
   126  		buildTmpl = append(buildTmpl, mainPart)
   127  		preStage = stage
   128  		preCache = build.Cache
   129  	}
   130  
   131  	if opts.User != "" && opts.UID != 0 {
   132  		userPart, err := makeUserPart(opts)
   133  		if err != nil {
   134  			return err
   135  		}
   136  		buildTmpl = append(buildTmpl, userPart)
   137  	}
   138  	dockerfile := strings.Join(buildTmpl, "\n")
   139  	return createDockerfile(dockerfile, buildDir)
   140  }
   141  
   142  func (e *Engine) preparedSource(ctx context.Context, build *types.Build, scm coresource.Source, buildDir string) (string, error) {
   143  	// parse repository name
   144  	// code locates under /:repositoryname
   145  	var cloneDir string
   146  	var err error
   147  	reponame := ""
   148  	if build.Repo != "" { //nolint
   149  		version := build.Version
   150  		if version == "" {
   151  			version = "HEAD"
   152  		}
   153  		reponame, err = utils.GetGitRepoName(build.Repo)
   154  		if err != nil {
   155  			return "", err
   156  		}
   157  
   158  		// clone code into cloneDir
   159  		// which is under buildDir and named as repository name
   160  		cloneDir = filepath.Join(buildDir, reponame)
   161  		if err := scm.SourceCode(ctx, build.Repo, cloneDir, version, build.Submodule); err != nil {
   162  			return "", err
   163  		}
   164  
   165  		if build.Security {
   166  			// ensure source code is safe
   167  			// we don't want any history files to be retrieved
   168  			if err := scm.Security(cloneDir); err != nil {
   169  				return "", err
   170  			}
   171  		}
   172  	}
   173  
   174  	// if artifact download url is provided, remove all source code to
   175  	// improve security
   176  	if len(build.Artifacts) > 0 {
   177  		artifactsDir := buildDir
   178  		if cloneDir != "" {
   179  			os.RemoveAll(cloneDir)
   180  			if err := os.MkdirAll(cloneDir, os.ModeDir); err != nil {
   181  				return "", err
   182  			}
   183  			artifactsDir = cloneDir
   184  		}
   185  		for _, artifact := range build.Artifacts {
   186  			if err := scm.Artifact(ctx, artifact, artifactsDir); err != nil {
   187  				return "", err
   188  			}
   189  		}
   190  	}
   191  
   192  	return reponame, nil
   193  }