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 }