Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
32 changes: 16 additions & 16 deletions core/generate/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"fmt"
"maps"
"slices"
"sort"
"strings"

"github.com/charmbracelet/log"
Expand All @@ -18,8 +17,15 @@ import (
)

type BuildStepOptions struct {
ResolvedPackages map[string]*resolver.ResolvedPackage
Caches *CacheContext
ResolvedPackages map[string]*resolver.ResolvedPackage
Caches *CacheContext
MiseBootstrapProject MiseBootstrapProjectConfig
}

type MiseBootstrapProjectConfig struct {
ConfigFiles []string
HasAptPackages bool
HasPackageHooks bool
}

type StepBuilder interface {
Expand Down Expand Up @@ -155,10 +161,12 @@ func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.Reso

// Create the actual build plan
buildPlan := plan.NewBuildPlan()
miseBootstrapProject := c.GetMiseStepBuilder().getMiseBootstrapProjectConfig()

buildStepOptions := &BuildStepOptions{
ResolvedPackages: resolvedPackages,
Caches: c.Caches,
ResolvedPackages: resolvedPackages,
Caches: c.Caches,
MiseBootstrapProject: miseBootstrapProject,
}

for _, stepBuilder := range c.Steps {
Expand All @@ -171,23 +179,15 @@ func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.Reso

buildPlan.Caches = c.Caches.Caches
buildPlan.Secrets = utils.RemoveDuplicates(c.Secrets)
c.Deploy.Build(buildPlan, buildStepOptions)
if err := c.Deploy.Build(buildPlan, buildStepOptions); err != nil {
return nil, nil, fmt.Errorf("failed to build deploy step: %w", err)
}

buildPlan.Normalize()

return buildPlan, resolvedPackages, nil
}

func (o *BuildStepOptions) NewAptInstallCommand(pkgs []string) plan.Command {
pkgs = utils.RemoveDuplicates(pkgs)
sort.Strings(pkgs)

// sh -c is required because && is a shell operator that needs a shell to interpret it
return plan.NewExecCommand("sh -c 'apt-get update && apt-get install -y "+strings.Join(pkgs, " ")+"'", plan.ExecOptions{
CustomName: "install apt packages: " + strings.Join(pkgs, " "),
})
}

func (c *GenerateContext) applyPackagesFromConfig() {
miseStep := c.GetMiseStepBuilder()

Expand Down
48 changes: 41 additions & 7 deletions core/generate/context_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ func TestGenerateContextAppliesConfiguredDeployBase(t *testing.T) {
require.Equal(t, plan.NewImageLayer("debian:bookworm-slim"), buildPlan.Deploy.Base)
})

t.Run("runtime apt step uses configured deploy base", func(t *testing.T) {
t.Run("runtime bootstrap step uses configured deploy base", func(t *testing.T) {
ctx := CreateTestContext(t, "../../examples/node-npm")
cfg := config.EmptyConfig()
cfg.Deploy.Base = &plan.Layer{Image: "debian:bookworm-slim"}
Expand All @@ -188,21 +188,55 @@ func TestGenerateContextAppliesConfiguredDeployBase(t *testing.T) {

buildPlan, _, err := ctx.Generate()
require.NoError(t, err)
require.Equal(t, plan.NewStepLayer("packages:apt:runtime"), buildPlan.Deploy.Base)
require.Equal(t, plan.NewStepLayer(MiseBootstrapRuntimeStepName), buildPlan.Deploy.Base)

var runtimeAptStep *plan.Step
var runtimeBootstrapStep *plan.Step
for i := range buildPlan.Steps {
if buildPlan.Steps[i].Name == "packages:apt:runtime" {
runtimeAptStep = &buildPlan.Steps[i]
if buildPlan.Steps[i].Name == MiseBootstrapRuntimeStepName {
runtimeBootstrapStep = &buildPlan.Steps[i]
break
}
}

require.NotNil(t, runtimeAptStep)
require.Equal(t, []plan.Layer{plan.NewImageLayer("debian:bookworm-slim")}, runtimeAptStep.Inputs)
require.NotNil(t, runtimeBootstrapStep)
require.Equal(
t,
[]plan.Layer{plan.NewImageLayer("debian:bookworm-slim")},
runtimeBootstrapStep.Inputs,
)
})
}

func TestGenerateContextAppliesProjectMiseBootstrapConfig(t *testing.T) {
ctx := CreateTestContext(t, "../../examples/config-file")
provider := &TestProvider{}
require.NoError(t, provider.Plan(ctx))

buildPlan, _, err := ctx.Generate()
require.NoError(t, err)

var buildBootstrapStep *plan.Step
var runtimeBootstrapStep *plan.Step
for i := range buildPlan.Steps {
switch buildPlan.Steps[i].Name {
case MiseBootstrapBuildStepName:
buildBootstrapStep = &buildPlan.Steps[i]
case MiseBootstrapRuntimeStepName:
runtimeBootstrapStep = &buildPlan.Steps[i]
}
}

require.NotNil(t, buildBootstrapStep)
require.NotNil(t, runtimeBootstrapStep)
require.Equal(t, []string{"*"}, buildBootstrapStep.Secrets)
require.Empty(t, runtimeBootstrapStep.Secrets)
require.Contains(
t,
runtimeBootstrapStep.Inputs,
miseBootstrapRepositoryLayer(),
)
}

func TestGenerateContextDockerignore(t *testing.T) {
t.Run("context with dockerignore", func(t *testing.T) {
ctx := CreateTestContext(t, "../../examples/dockerignore")
Expand Down
38 changes: 27 additions & 11 deletions core/generate/deploy_builder.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package generate

import (
"fmt"

"github.com/railwayapp/railpack/core/plan"
)

Expand Down Expand Up @@ -51,19 +53,31 @@ func (b *DeployBuilder) AddAptPackages(packages []string) {
b.AptPackages = append(b.AptPackages, packages...)
}

func (b *DeployBuilder) Build(p *plan.BuildPlan, options *BuildStepOptions) {
func (b *DeployBuilder) Build(p *plan.BuildPlan, options *BuildStepOptions) error {
baseLayer := b.Base

if len(b.AptPackages) > 0 {
runtimeAptStep := plan.NewStep("packages:apt:runtime")
runtimeAptStep.Inputs = []plan.Layer{baseLayer}
runtimeAptStep.AddCommands([]plan.Command{
options.NewAptInstallCommand(b.AptPackages),
})
runtimeAptStep.Caches = options.Caches.GetAptCaches()
runtimeAptStep.Secrets = []string{}
p.Steps = append(p.Steps, *runtimeAptStep)
baseLayer = plan.NewStepLayer(runtimeAptStep.Name)
bootstrapBuilder := NewMiseBootstrapStepBuilder(
MiseBootstrapRuntimeStepName,
baseLayer,
b.AptPackages,
options.MiseBootstrapProject.ConfigFiles,
)
bootstrapBuilder.CopyMise = true
bootstrapBuilder.RunHooks = false
bootstrapBuilder.ApplyProjectPackages = options.MiseBootstrapProject.HasAptPackages
bootstrapBuilder.HasProjectHooks = options.MiseBootstrapProject.HasPackageHooks
if bootstrapBuilder.IsRequired() && bootstrapBuilder.HasProjectHooks {
// Repository hooks run in the builder where bootstrap utilities are available.
bootstrapBuilder.Inputs = []plan.Layer{miseBootstrapRepositoryLayer()}
}
if bootstrapBuilder.IsRequired() {
bootstrapStep, err := bootstrapBuilder.Build(options)
if err != nil {
return fmt.Errorf("failed to build runtime bootstrap step: %w", err)
}

p.Steps = append(p.Steps, *bootstrapStep)
baseLayer = plan.NewStepLayer(bootstrapStep.Name)
}

p.Deploy.Base = baseLayer
Expand All @@ -72,4 +86,6 @@ func (b *DeployBuilder) Build(p *plan.BuildPlan, options *BuildStepOptions) {
p.Deploy.StartCmd = b.StartCmd
p.Deploy.Variables = b.Variables
p.Deploy.Paths = b.Paths

return nil
}
29 changes: 22 additions & 7 deletions core/generate/image_step_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,33 @@ func (b *ImageStepBuilder) Name() string {
func (b *ImageStepBuilder) Build(p *plan.BuildPlan, options *BuildStepOptions) error {
image := b.ResolveStepImage(options)

step := plan.NewStep(b.DisplayName)
step.Secrets = []string{}
step.Inputs = []plan.Layer{
bootstrapBuilder := NewMiseBootstrapStepBuilder(
b.DisplayName,
plan.NewImageLayer(image),
b.AptPackages,
options.MiseBootstrapProject.ConfigFiles,
)
bootstrapBuilder.CopyMise = true
bootstrapBuilder.RunHooks = false
bootstrapBuilder.ApplyProjectPackages = options.MiseBootstrapProject.HasAptPackages
bootstrapBuilder.HasProjectHooks = options.MiseBootstrapProject.HasPackageHooks
if bootstrapBuilder.IsRequired() && bootstrapBuilder.HasProjectHooks {
// Run repository hooks once on the full builder image, then reuse their output.
bootstrapBuilder.Inputs = []plan.Layer{miseBootstrapRepositoryLayer()}
}

if len(b.AptPackages) > 0 {
step.Commands = []plan.Command{
options.NewAptInstallCommand(b.AptPackages),
if bootstrapBuilder.IsRequired() {
step, err := bootstrapBuilder.Build(options)
if err != nil {
return err
}

p.Steps = append(p.Steps, *step)
return nil
}

step := plan.NewStep(b.DisplayName)
step.Secrets = []string{}
step.Inputs = []plan.Layer{plan.NewImageLayer(image)}
p.Steps = append(p.Steps, *step)

return nil
Expand Down
46 changes: 46 additions & 0 deletions core/generate/image_step_builder_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package generate

import (
"testing"

"github.com/railwayapp/railpack/core/plan"
"github.com/stretchr/testify/require"
)

func TestImageStepBuilderReusesBuildBootstrapRepositories(t *testing.T) {
builder := &ImageStepBuilder{
DisplayName: "packages:image",
ResolveStepImage: func(_ *BuildStepOptions) string {
return "debian:bookworm-slim"
},
AptPackages: []string{"curl"},
}
options := &BuildStepOptions{
Caches: NewCacheContext(),
MiseBootstrapProject: MiseBootstrapProjectConfig{
ConfigFiles: []string{"mise.toml"},
HasAptPackages: true,
HasPackageHooks: true,
},
}
buildPlan := plan.NewBuildPlan()

err := builder.Build(buildPlan, options)
require.NoError(t, err)
require.Len(t, buildPlan.Steps, 1)

step := buildPlan.Steps[0]
require.Equal(t, []plan.Layer{
plan.NewImageLayer("debian:bookworm-slim"),
miseBootstrapRepositoryLayer(),
}, step.Inputs)
require.Empty(t, step.Secrets)

bootstrapCommand, ok := step.Commands[3].(plan.ExecCommand)
require.True(t, ok)
require.Contains(
t,
bootstrapCommand.Cmd,
"bootstrap packages apply --manager apt --yes --update",
)
}
Loading
Loading