Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detector changes for run image extension #1011

Merged
merged 22 commits into from
Mar 8, 2023
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f22c4f9
Make a single constructor for lifecycle inputs
natalieparellano Feb 13, 2023
433bf3c
Read values from environment
natalieparellano Feb 14, 2023
f40616e
Buildpack API: run.Dockerfiles are allowed instructions on versions >…
natalieparellano Feb 14, 2023
29a94a6
Platform API: the detector accepts a new -run flag
natalieparellano Feb 14, 2023
569f985
Move responsibility for validating Dockerfiles into the buildpack pac…
natalieparellano Feb 15, 2023
c72a10e
When verifying Dockerfiles, return the new base image name if necessary
natalieparellano Feb 15, 2023
4c5a564
When determining the new runtime base image, use criteria outlined in…
natalieparellano Feb 15, 2023
01ab4b6
Platform API: the schema of analyzed.toml is updated to include run-i…
natalieparellano Feb 15, 2023
9ba40e3
TESTME: Update analyzed.toml with new run image if needed
natalieparellano Feb 15, 2023
eea3182
If extensions are used to switch the runtime base image, the detector…
natalieparellano Feb 15, 2023
647a3db
Add fixture to test re-writing of analyzed.toml
natalieparellano Feb 15, 2023
9157796
Move updating analyzed.toml into lifecycle package for easier testing
natalieparellano Feb 15, 2023
d0ce558
Merge branch 'main' into runext/detect-996
natalieparellano Mar 1, 2023
9ba92d1
Fix acceptance
natalieparellano Mar 1, 2023
b8f029f
Don't redefine -layers
natalieparellano Mar 1, 2023
5780910
Revert "Replace print with logger in image_cache.go, fixes formatting…
natalieparellano Mar 1, 2023
3f0c573
Merge branch 'main' into runext/detect-996
natalieparellano Mar 6, 2023
4226840
Merge branch 'main' into runext/detect-996
natalieparellano Mar 7, 2023
512f8a0
Revert "Revert "Replace print with logger in image_cache.go, fixes fo…
natalieparellano Mar 7, 2023
4d225fb
Rename image -> images in run.toml
natalieparellano Mar 7, 2023
34e396a
Don't enforce constraints for older extensions
natalieparellano Mar 8, 2023
b1c2c2e
Rename function for clarity
natalieparellano Mar 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions acceptance/detector_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -362,11 +362,13 @@ fail: fail_detect_buildpack@some_version
"-extensions=/cnb/extensions",
"-generated=/layers/generated",
"-log-level=debug",
"-run=/layers/run.toml", // /cnb/run.toml is the default location of run.toml
),
)

t.Log("runs /bin/detect for buildpacks and extensions")
h.AssertStringContains(t, output, "Platform requested experimental feature 'Dockerfiles'")
h.AssertStringContains(t, output, "FOO=val-from-build-config")
natalieparellano marked this conversation as resolved.
Show resolved Hide resolved
h.AssertStringContains(t, output, "simple_extension: output from /bin/detect")
t.Log("writes group.toml")
foundGroupTOML := filepath.Join(copyDir, "layers", "group.toml")
Expand Down
4 changes: 2 additions & 2 deletions acceptance/testdata/analyzer/container/cnb/run.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[[image]]
[[images]]
image = "some-run-image-from-run-toml"

[[image]]
[[images]]
image = "some-other-run-image"
4 changes: 2 additions & 2 deletions acceptance/testdata/creator/container/cnb/run.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[[image]]
[[images]]
image = "some-run-image-from-run-toml"

[[image]]
[[images]]
image = "some-other-run-image"
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
val-from-build-config
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
#!/usr/bin/env bash

echo "ENV"
env

plan_path=$2

cat >> "${plan_path}" <<EOL
Expand Down
5 changes: 5 additions & 0 deletions acceptance/testdata/detector/container/cnb/run.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[[images]]
image = "some-run-image-from-extension"

[[images]]
image = "some-other-run-image"
2 changes: 2 additions & 0 deletions acceptance/testdata/detector/container/layers/analyzed.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run-image]
reference = "some-old-run-image"
22 changes: 10 additions & 12 deletions analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -189,15 +189,15 @@ func (f *AnalyzerFactory) setRun(analyzer *Analyzer, imageRef string) error {
// Analyze fetches the layers metadata from the previous image and writes analyzed.toml.
func (a *Analyzer) Analyze() (platform.AnalyzedMetadata, error) {
var (
err error
appMeta platform.LayersMetadata
cacheMeta platform.CacheMetadata
previousImageIDReference string
runImageIDReference string
err error
appMeta platform.LayersMetadata
cacheMeta platform.CacheMetadata
previousImageRef string
runImageRef string
)

if a.PreviousImage != nil { // Previous image is optional in Platform API >= 0.7
if previousImageIDReference, err = a.getImageIdentifier(a.PreviousImage); err != nil {
if previousImageRef, err = a.getImageIdentifier(a.PreviousImage); err != nil {
return platform.AnalyzedMetadata{}, errors.Wrap(err, "identifying previous image")
}

Expand All @@ -214,7 +214,7 @@ func (a *Analyzer) Analyze() (platform.AnalyzedMetadata, error) {
}

if a.RunImage != nil {
runImageIDReference, err = a.getImageIdentifier(a.RunImage)
runImageRef, err = a.getImageIdentifier(a.RunImage)
if err != nil {
return platform.AnalyzedMetadata{}, errors.Wrap(err, "identifying run image")
}
Expand All @@ -233,11 +233,9 @@ func (a *Analyzer) Analyze() (platform.AnalyzedMetadata, error) {
}

return platform.AnalyzedMetadata{
PreviousImage: &platform.ImageIdentifier{
Reference: previousImageIDReference,
},
RunImage: &platform.RunImage{Reference: runImageIDReference},
Metadata: appMeta,
PreviousImage: &platform.ImageIdentifier{Reference: previousImageRef},
RunImage: &platform.RunImage{Reference: runImageRef},
Metadata: appMeta,
}, nil
}

Expand Down
2 changes: 1 addition & 1 deletion api/apis.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import (

var (
Platform = newApisMustParse([]string{"0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10", "0.11", "0.12"}, []string{"0.3", "0.4", "0.5", "0.6"})
Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9"}, []string{"0.2", "0.3", "0.4", "0.5", "0.6"})
Buildpack = newApisMustParse([]string{"0.2", "0.3", "0.4", "0.5", "0.6", "0.7", "0.8", "0.9", "0.10"}, []string{"0.2", "0.3", "0.4", "0.5", "0.6"})
)

type APIs struct {
Expand Down
109 changes: 80 additions & 29 deletions buildpack/dockerfile.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,45 @@ package buildpack

import (
"bytes"
"errors"
"fmt"
"os"
"strings"

"github.com/moby/buildkit/frontend/dockerfile/instructions"
"github.com/moby/buildkit/frontend/dockerfile/parser"

"github.com/buildpacks/lifecycle/api"
"github.com/buildpacks/lifecycle/log"
)

const (
DockerfileKindBuild = "build"
DockerfileKindRun = "run"

buildDockerfileName = "build.Dockerfile"
runDockerfileName = "run.Dockerfile"

baseImageArgName = "base_image"
baseImageArgRef = "${base_image}"

errArgumentsNotPermitted = "run.Dockerfile should not expect arguments"
errBuildMissingRequiredARGCommand = "build.Dockerfile did not start with required ARG command"
errBuildMissingRequiredFROMCommand = "build.Dockerfile did not contain required FROM ${base_image} command"
errMissingRequiredStage = "%s should have at least one stage"
errMissingRequiredInstruction = "%s should have at least one instruction"
errMultiStageNotPermitted = "%s is not permitted to use multistage build"
errRunOtherInstructionsNotPermitted = "run.Dockerfile is not permitted to have instructions other than FROM"
warnCommandNotRecommended = "%s command %s on line %d is not recommended"
)

var permittedCommandsBuild = []string{"FROM", "ADD", "ARG", "COPY", "ENV", "LABEL", "RUN", "SHELL", "USER", "WORKDIR"}
var permittedCommands = []string{"FROM", "ADD", "ARG", "COPY", "ENV", "LABEL", "RUN", "SHELL", "USER", "WORKDIR"}

type DockerfileInfo struct {
ExtensionID string
Kind string
Path string
NewBase string
}

type ExtendConfig struct {
Expand Down Expand Up @@ -64,80 +82,113 @@ func VerifyBuildDockerfile(dockerfile string, logger log.Logger) error {

// validate only 1 FROM
if len(stages) > 1 {
return fmt.Errorf("build.Dockerfile is not permitted to use multistage build")
return fmt.Errorf(errMultiStageNotPermitted, buildDockerfileName)
}

// validate only permitted Commands
for _, stage := range stages {
for _, command := range stage.Commands {
found := false
for _, permittedCommand := range permittedCommandsBuild {
for _, permittedCommand := range permittedCommands {
if permittedCommand == strings.ToUpper(command.Name()) {
found = true
break
}
}
if !found {
logger.Warnf("build.Dockerfile command %s on line %d is not recommended", strings.ToUpper(command.Name()), command.Location()[0].Start.Line)
logger.Warnf(warnCommandNotRecommended, buildDockerfileName, strings.ToUpper(command.Name()), command.Location()[0].Start.Line)
}
}
}

// validate build.Dockerfile preamble
if len(margs) != 1 {
return fmt.Errorf("build.Dockerfile did not start with required ARG command")
return errors.New(errBuildMissingRequiredARGCommand)
}
if margs[0].Args[0].Key != "base_image" {
return fmt.Errorf("build.Dockerfile did not start with required ARG base_image command")
if margs[0].Args[0].Key != baseImageArgName {
return errors.New(errBuildMissingRequiredARGCommand)
}
// sanity check to prevent panic
if len(stages) == 0 {
return fmt.Errorf("build.Dockerfile should have at least one stage")
return fmt.Errorf(errMissingRequiredStage, buildDockerfileName)
}
if stages[0].BaseName != "${base_image}" {
return fmt.Errorf("build.Dockerfile did not contain required FROM ${base_image} command")

if stages[0].BaseName != baseImageArgRef {
return errors.New(errBuildMissingRequiredFROMCommand)
}

return nil
}

func VerifyRunDockerfile(dockerfile string) error {
func VerifyRunDockerfile(dockerfile string, buildpackAPI *api.Version, logger log.Logger) (string, error) {
if buildpackAPI.LessThan("0.10") {
natalieparellano marked this conversation as resolved.
Show resolved Hide resolved
return verifyRunDockerfile09(dockerfile)
}
return verifyRunDockerfile(dockerfile, logger)
}

func verifyRunDockerfile(dockerfile string, logger log.Logger) (string, error) {
stages, _, err := parseDockerfile(dockerfile)
if err != nil {
return "", err
}

// validate only 1 FROM
if len(stages) > 1 {
return "", fmt.Errorf(errMultiStageNotPermitted, runDockerfileName)
}
if len(stages) == 0 {
return "", fmt.Errorf(errMissingRequiredStage, runDockerfileName)
}

var newBase string
// validate only permitted Commands
for _, stage := range stages {
if stage.BaseName != baseImageArgRef {
newBase = stage.BaseName
}
for _, command := range stage.Commands {
found := false
for _, permittedCommand := range permittedCommands {
if permittedCommand == strings.ToUpper(command.Name()) {
found = true
break
}
}
if !found {
logger.Warnf(warnCommandNotRecommended, runDockerfileName, strings.ToUpper(command.Name()), command.Location()[0].Start.Line)
}
}
}

return newBase, nil
}

func verifyRunDockerfile09(dockerfile string) (string, error) {
stages, margs, err := parseDockerfile(dockerfile)
if err != nil {
return err
return "", err
}

// validate only 1 FROM
if len(stages) > 1 {
return fmt.Errorf("run.Dockerfile is not permitted to use multistage build")
return "", fmt.Errorf(errMultiStageNotPermitted, runDockerfileName)
}

// validate FROM does not expect argument
if len(margs) > 0 {
return fmt.Errorf("run.Dockerfile should not expect arguments")
return "", errors.New(errArgumentsNotPermitted)
}

// sanity check to prevent panic
if len(stages) == 0 {
return fmt.Errorf("run.Dockerfile should have at least one stage")
return "", fmt.Errorf(errMissingRequiredStage, runDockerfileName)
}

// validate no instructions in stage
if len(stages[0].Commands) != 0 {
return fmt.Errorf("run.Dockerfile is not permitted to have instructions other than FROM")
return "", fmt.Errorf(errRunOtherInstructionsNotPermitted)
}

return nil
}

func RetrieveFirstFromImageNameFromDockerfile(dockerfile string) (string, error) {
ins, _, err := parseDockerfile(dockerfile)
if err != nil {
return "", err
}
// sanity check to prevent panic
if len(ins) == 0 {
return "", fmt.Errorf("expected at least one instruction")
}
return ins[0].BaseName, nil
return stages[0].BaseName, nil
}
Loading