get.porter.sh/porter@v1.3.0/pkg/build/dockerfile-generator.go (about) 1 package build 2 3 import ( 4 "bufio" 5 "bytes" 6 "context" 7 "errors" 8 "fmt" 9 "io" 10 "path/filepath" 11 "strings" 12 13 "get.porter.sh/porter/pkg" 14 "get.porter.sh/porter/pkg/config" 15 "get.porter.sh/porter/pkg/experimental" 16 "get.porter.sh/porter/pkg/manifest" 17 "get.porter.sh/porter/pkg/mixin/query" 18 "get.porter.sh/porter/pkg/pkgmgmt" 19 "get.porter.sh/porter/pkg/templates" 20 "get.porter.sh/porter/pkg/tracing" 21 ) 22 23 const ( 24 // DefaultDockerfileSyntax is the default syntax for Dockerfiles used by Porter 25 // either when generating a Dockerfile from scratch, or when a template does 26 // not define a syntax 27 DefaultDockerfileSyntax = "docker/dockerfile-upstream:1.4.0" 28 ) 29 30 type DockerfileGenerator struct { 31 *config.Config 32 *manifest.Manifest 33 *templates.Templates 34 Mixins pkgmgmt.PackageManager 35 } 36 37 func NewDockerfileGenerator(config *config.Config, m *manifest.Manifest, tmpl *templates.Templates, mp pkgmgmt.PackageManager) *DockerfileGenerator { 38 return &DockerfileGenerator{ 39 Config: config, 40 Manifest: m, 41 Templates: tmpl, 42 Mixins: mp, 43 } 44 } 45 46 func (g *DockerfileGenerator) GenerateDockerFile(ctx context.Context) error { 47 ctx, span := tracing.StartSpan(ctx) 48 defer span.EndSpan() 49 50 var lines []string 51 var err error 52 if g.Config.IsFeatureEnabled(experimental.FlagFullControlDockerfile) { 53 span.Warnf("WARNING: Experimental feature \"%s\" enabled: Dockerfile will be used without changes by Porter", 54 experimental.FullControlDockerfile) 55 lines, err = g.readRawDockerfile(ctx) 56 if err != nil { 57 return span.Error(fmt.Errorf("error reading the Dockerfile: %w", err)) 58 } 59 } else { 60 lines, err = g.buildDockerfile(ctx) 61 if err != nil { 62 return span.Error(fmt.Errorf("error generating the Dockerfile: %w", err)) 63 } 64 } 65 66 contents := strings.Join(lines, "\n") 67 68 // Output the generated dockerfile 69 span.Debug(contents) 70 71 err = g.FileSystem.WriteFile(DOCKER_FILE, []byte(contents), pkg.FileModeWritable) 72 if err != nil { 73 return span.Error(fmt.Errorf("couldn't write the Dockerfile: %w", err)) 74 } 75 76 return nil 77 } 78 79 func (g *DockerfileGenerator) readRawDockerfile(ctx context.Context) ([]string, error) { 80 if g.Manifest.Dockerfile == "" { 81 return nil, errors.New("no Dockerfile specified in the manifest") 82 } 83 exists, err := g.FileSystem.Exists(g.Manifest.Dockerfile) 84 if err != nil { 85 return nil, fmt.Errorf("error checking if Dockerfile exists: %q: %w", g.Manifest.Dockerfile, err) 86 } 87 if !exists { 88 return nil, fmt.Errorf("the Dockerfile specified in the manifest doesn't exist: %q", g.Manifest.Dockerfile) 89 } 90 91 file, err := g.FileSystem.Open(g.Manifest.Dockerfile) 92 if err != nil { 93 return nil, err 94 } 95 defer file.Close() 96 97 var lines []string 98 scanner := bufio.NewScanner(file) 99 for scanner.Scan() { 100 lines = append(lines, scanner.Text()) 101 } 102 103 return lines, scanner.Err() 104 } 105 106 func (g *DockerfileGenerator) buildDockerfile(ctx context.Context) ([]string, error) { 107 log := tracing.LoggerFromContext(ctx) 108 log.Debug("Generating Dockerfile") 109 110 lines, err := g.getBaseDockerfile(ctx) 111 if err != nil { 112 return nil, err 113 } 114 115 lines = append(lines, g.buildPorterSection()...) 116 lines = append(lines, g.buildCNABSection()...) 117 lines = append(lines, g.buildWORKDIRSection()) 118 lines = append(lines, g.buildCMDSection()) 119 120 return lines, nil 121 } 122 123 func (g *DockerfileGenerator) getBaseDockerfile(ctx context.Context) ([]string, error) { 124 log := tracing.LoggerFromContext(ctx) 125 126 var reader io.Reader 127 if g.Manifest.Dockerfile != "" { 128 exists, err := g.FileSystem.Exists(g.Manifest.Dockerfile) 129 if err != nil { 130 return nil, fmt.Errorf("error checking if Dockerfile exists: %q: %w", g.Manifest.Dockerfile, err) 131 } 132 if !exists { 133 return nil, fmt.Errorf("the Dockerfile specified in the manifest doesn't exist: %q", g.Manifest.Dockerfile) 134 } 135 136 file, err := g.FileSystem.Open(g.Manifest.Dockerfile) 137 if err != nil { 138 return nil, err 139 } 140 defer file.Close() 141 reader = file 142 } else { 143 contents, err := g.Templates.GetDockerfile() 144 if err != nil { 145 return nil, fmt.Errorf("error loading default Dockerfile template: %w", err) 146 } 147 reader = bytes.NewReader(contents) 148 } 149 scanner := bufio.NewScanner(reader) 150 var lines []string 151 var syntaxFound bool 152 for scanner.Scan() { 153 line := scanner.Text() 154 if !syntaxFound && strings.HasPrefix(line, "# syntax=") { 155 syntaxFound = true 156 } 157 lines = append(lines, line) 158 } 159 160 // If their template doesn't declare a syntax, use the default 161 if !syntaxFound { 162 log.Warnf("No syntax was declared in the template Dockerfile, using %s", DefaultDockerfileSyntax) 163 lines = append([]string{fmt.Sprintf("# syntax=%s", DefaultDockerfileSyntax)}, lines...) 164 } 165 166 return g.replaceTokens(ctx, lines) 167 } 168 169 func (g *DockerfileGenerator) buildPorterSection() []string { 170 // The user-provided manifest may be located separate from the build context directory. 171 // Therefore, we only need to add lines if the relative manifest path exists inside of 172 // the current working directory. 173 manifestPath := g.FileSystem.Abs(g.Manifest.ManifestPath) 174 if relManifestPath, err := filepath.Rel(g.Getwd(), manifestPath); err == nil { 175 if !strings.Contains(relManifestPath, "..") { 176 return []string{ 177 // Remove the user-provided Porter manifest as the canonical version 178 // will migrate via its location in .cnab 179 fmt.Sprintf(`RUN rm ${BUNDLE_DIR}/%s`, relManifestPath), 180 } 181 } 182 } 183 return []string{} 184 } 185 186 func (g *DockerfileGenerator) buildCNABSection() []string { 187 copyCNAB := "COPY .cnab /cnab" 188 if g.GetBuildDriver() == config.BuildDriverBuildkit { 189 copyCNAB = "COPY --link .cnab /cnab" 190 } 191 192 return []string{ 193 // Putting RUN before COPY here as a workaround for https://github.com/moby/moby/issues/37965, back to back COPY statements in the same directory (e.g. /cnab) _may_ result in an error from Docker depending on unpredictable factors 194 `RUN rm -fr ${BUNDLE_DIR}/.cnab`, 195 // Copy the non-user cnab files, like mixins and porter.yaml, from the local .cnab directory into the bundle 196 copyCNAB, 197 // Ensure that regardless of the container's UID, the root group (default group for arbitrary users that do not exist in the container) has the same permissions as the owner 198 // See https://developers.redhat.com/blog/2020/10/26/adapting-docker-and-kubernetes-containers-to-run-on-red-hat-openshift-container-platform#group_ownership_and_file_permission 199 `RUN chgrp -R ${BUNDLE_GID} /cnab && chmod -R g=u /cnab`, 200 // default to running as the nonroot user that the porter agent uses. 201 // When running in kubernetes, if you specify a different UID, make sure to set fsGroup to the same UID, and runasGroup to 0 202 `USER ${BUNDLE_UID}`, 203 } 204 } 205 206 func (g *DockerfileGenerator) buildWORKDIRSection() string { 207 return `WORKDIR ${BUNDLE_DIR}` 208 } 209 210 func (g *DockerfileGenerator) buildCMDSection() string { 211 return `CMD ["/cnab/app/run"]` 212 } 213 214 func (g *DockerfileGenerator) buildMixinsSection(ctx context.Context) ([]string, error) { 215 q := query.New(g.Context, g.Mixins) 216 q.RequireAllMixinResponses = true 217 q.LogMixinErrors = true 218 results, err := q.Execute(ctx, "build", query.NewManifestGenerator(g.Manifest)) 219 if err != nil { 220 return nil, err 221 } 222 223 lines := make([]string, 0) 224 for _, result := range results { 225 l := strings.Split(result.Stdout, "\n") 226 lines = append(lines, l...) 227 } 228 return lines, nil 229 } 230 231 func (g *DockerfileGenerator) buildInitSection() []string { 232 return []string{ 233 "ARG BUNDLE_DIR", 234 "ARG BUNDLE_UID=65532", 235 "ARG BUNDLE_USER=nonroot", 236 "ARG BUNDLE_GID=0", 237 // Create a non-root user that is in the root group with the specified id and a home directory 238 "RUN useradd ${BUNDLE_USER} -m -u ${BUNDLE_UID} -g ${BUNDLE_GID} -o", 239 } 240 } 241 242 func (g *DockerfileGenerator) PrepareFilesystem() error { 243 fmt.Fprintf(g.Out, "Copying porter runtime ===> \n") 244 245 runTmpl, err := g.Templates.GetRunScript() 246 if err != nil { 247 return err 248 } 249 250 err = g.FileSystem.MkdirAll(LOCAL_APP, pkg.FileModeDirectory) 251 if err != nil { 252 return err 253 } 254 255 err = g.FileSystem.WriteFile(LOCAL_RUN, runTmpl, pkg.FileModeExecutable) 256 if err != nil { 257 return fmt.Errorf("failed to write %s: %w", LOCAL_RUN, err) 258 } 259 260 homeDir, err := g.GetHomeDir() 261 if err != nil { 262 return err 263 } 264 err = g.Context.CopyDirectory(filepath.Join(homeDir, "runtimes"), filepath.Join(LOCAL_APP, "runtimes"), false) 265 if err != nil { 266 return err 267 } 268 269 fmt.Fprintf(g.Out, "Copying mixins ===> \n") 270 for _, m := range g.Manifest.Mixins { 271 err := g.copyMixin(m.Name) 272 if err != nil { 273 return err 274 } 275 } 276 277 return nil 278 } 279 280 func (g *DockerfileGenerator) copyMixin(mixin string) error { 281 fmt.Fprintf(g.Out, "Copying mixin %s ===> \n", mixin) 282 mixinDir, err := g.Mixins.GetPackageDir(mixin) 283 if err != nil { 284 return err 285 } 286 287 err = g.Context.CopyDirectory(mixinDir, LOCAL_MIXINS, true) 288 if err != nil { 289 return fmt.Errorf("could not copy mixin directory contents for %s: %w", mixin, err) 290 } 291 292 return nil 293 } 294 295 func (g *DockerfileGenerator) getIndexOfToken(lines []string, token string) int { 296 for lineNumber, lineContent := range lines { 297 if strings.HasPrefix(strings.TrimSpace(lineContent), token) { 298 return lineNumber 299 } 300 } 301 return -1 302 } 303 304 // replaceTokens looks for lines like # PORTER_MIXINS and replaces them in the 305 // template with the appropriate set of Dockerfile lines. 306 func (g *DockerfileGenerator) replaceTokens(ctx context.Context, lines []string) ([]string, error) { 307 mixinLines, err := g.buildMixinsSection(ctx) 308 if err != nil { 309 return nil, fmt.Errorf("error generating Dockerfile content for mixins: %w", err) 310 } 311 312 fromToken := g.getIndexOfToken(lines, "FROM") + 1 313 314 substitutions := []struct { 315 token string 316 lines []string 317 defaultIndex int 318 replace bool 319 }{ 320 {token: PORTER_INIT_TOKEN, lines: g.buildInitSection(), defaultIndex: fromToken, replace: true}, 321 {token: PORTER_MIXINS_TOKEN, lines: mixinLines, defaultIndex: -1, replace: true}, 322 } 323 324 for _, substitution := range substitutions { 325 index := g.getIndexOfToken(lines, substitution.token) 326 327 // If we can't find the token, use the default for that token 328 if index == -1 { 329 index = substitution.defaultIndex 330 substitution.replace = false 331 } 332 333 if index == -1 { 334 lines = append(lines, substitution.lines...) 335 } else { 336 prefix := make([]string, index) 337 copy(prefix, lines) 338 339 if substitution.replace { 340 // Do not keep the line at the insertion index, replace it with the new lines instead 341 index = index + 1 342 } 343 344 suffix := lines[index:] 345 lines = append(prefix, append(substitution.lines, suffix...)...) 346 } 347 } 348 349 return lines, nil 350 }