Files
pack/core/generate/context.go
T
2025-03-04 01:38:17 -05:00

248 lines
6.7 KiB
Go

package generate
import (
"fmt"
"maps"
"slices"
"sort"
"strings"
"github.com/charmbracelet/log"
a "github.com/railwayapp/railpack/core/app"
"github.com/railwayapp/railpack/core/config"
"github.com/railwayapp/railpack/core/logger"
"github.com/railwayapp/railpack/core/mise"
"github.com/railwayapp/railpack/core/plan"
"github.com/railwayapp/railpack/core/resolver"
"github.com/railwayapp/railpack/core/utils"
)
type BuildStepOptions struct {
ResolvedPackages map[string]*resolver.ResolvedPackage
Caches *CacheContext
}
type StepBuilder interface {
Name() string
Build(options *BuildStepOptions) (*plan.Step, error)
}
type GenerateContext struct {
App *a.App
Env *a.Environment
Config *config.Config
BaseImage string
Steps []StepBuilder
Deploy *DeployBuilder
Caches *CacheContext
Secrets []string
SubContexts []string
Metadata *Metadata
Resolver *resolver.Resolver
MiseStepBuilder *MiseStepBuilder
Logger *logger.Logger
}
type Command interface {
IsSpread() bool
}
type CommandWrapper struct {
Command plan.Command
}
func (c CommandWrapper) IsSpread() bool {
if execCmd, ok := c.Command.(plan.ExecCommand); ok {
return execCmd.Cmd == plan.ShellCommandString("...") || execCmd.Cmd == "..."
}
return false
}
func NewGenerateContext(app *a.App, env *a.Environment, config *config.Config, logger *logger.Logger) (*GenerateContext, error) {
resolver, err := resolver.NewResolver(mise.InstallDir)
if err != nil {
return nil, err
}
ctx := &GenerateContext{
App: app,
Env: env,
Config: config,
Steps: make([]StepBuilder, 0),
Deploy: NewDeployBuilder(),
Caches: NewCacheContext(),
Secrets: []string{},
Metadata: NewMetadata(),
Resolver: resolver,
Logger: logger,
}
// The default runtime image should include the runtime apt packages
ctx.Deploy.Inputs = append(ctx.Deploy.Inputs, ctx.DefaultRuntimeInput())
return ctx, nil
}
func (c *GenerateContext) GetMiseStepBuilder() *MiseStepBuilder {
if c.MiseStepBuilder == nil {
c.MiseStepBuilder = c.newMiseStepBuilder()
}
return c.MiseStepBuilder
}
func (c *GenerateContext) EnterSubContext(subContext string) *GenerateContext {
c.SubContexts = append(c.SubContexts, subContext)
return c
}
func (c *GenerateContext) ExitSubContext() *GenerateContext {
c.SubContexts = c.SubContexts[:len(c.SubContexts)-1]
return c
}
func (c *GenerateContext) GetStepName(name string) string {
subContextNames := strings.Join(c.SubContexts, ":")
if subContextNames != "" {
return name + ":" + subContextNames
}
return name
}
func (c *GenerateContext) GetStepByName(name string) *StepBuilder {
for _, step := range c.Steps {
if step.Name() == name {
return &step
}
}
return nil
}
func (c *GenerateContext) ResolvePackages() (map[string]*resolver.ResolvedPackage, error) {
return c.Resolver.ResolvePackages()
}
// Generate a build plan from the context
func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.ResolvedPackage, error) {
c.applyConfig()
// Resolve all package versions into a fully qualified and valid version
resolvedPackages, err := c.ResolvePackages()
if err != nil {
return nil, nil, err
}
// Create the actual build plan
buildPlan := plan.NewBuildPlan()
buildStepOptions := &BuildStepOptions{
ResolvedPackages: resolvedPackages,
Caches: c.Caches,
}
for _, stepBuilder := range c.Steps {
step, err := stepBuilder.Build(buildStepOptions)
if err != nil {
return nil, nil, fmt.Errorf("failed to build step: %w", err)
}
buildPlan.AddStep(*step)
}
buildPlan.Caches = c.Caches.Caches
buildPlan.Secrets = utils.RemoveDuplicates(c.Secrets)
buildPlan.Deploy = c.Deploy.Build()
return buildPlan, resolvedPackages, nil
}
func (c *GenerateContext) DefaultRuntimeInput() plan.Input {
return c.DefaultRuntimeInputWithPackages([]string{})
}
func (c *GenerateContext) DefaultRuntimeInputWithPackages(additionalAptPackages []string) plan.Input {
aptPackages := append(c.Config.Deploy.AptPackages, additionalAptPackages...)
if len(aptPackages) == 0 {
return plan.NewImageInput(plan.RAILPACK_RUNTIME_IMAGE)
}
runtimeAptStep := c.NewAptStepBuilder("runtime")
runtimeAptStep.Packages = aptPackages
runtimeAptStep.AddInput(plan.NewImageInput(plan.RAILPACK_RUNTIME_IMAGE))
return plan.NewStepInput(runtimeAptStep.Name())
}
func (o *BuildStepOptions) NewAptInstallCommand(pkgs []string) plan.Command {
pkgs = utils.RemoveDuplicates(pkgs)
sort.Strings(pkgs)
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) applyConfig() {
miseStep := c.GetMiseStepBuilder()
for _, pkg := range slices.Sorted(maps.Keys(c.Config.Packages)) {
version := c.Config.Packages[pkg]
pkgRef := miseStep.Default(pkg, version)
miseStep.Version(pkgRef, version, "custom config")
}
// Apply the cache config to the context
maps.Copy(c.Caches.Caches, c.Config.Caches)
c.Secrets = plan.SpreadStrings(c.Config.Secrets, c.Secrets)
// Apply step config to the context
for _, name := range slices.Sorted(maps.Keys(c.Config.Steps)) {
configStep := c.Config.Steps[name]
var commandStepBuilder *CommandStepBuilder
if existingStep := c.GetStepByName(name); existingStep != nil {
if csb, ok := (*existingStep).(*CommandStepBuilder); ok {
commandStepBuilder = csb
} else {
log.Warnf("Step `%s` exists, but it is not a command step. Skipping...", name)
continue
}
} else {
// If no build step found, create a new one
// Run the build in the builder context and copy the /app contents to the final image
commandStepBuilder = c.NewCommandStep(name)
commandStepBuilder.AddInput(plan.NewStepInput(miseStep.Name()))
c.Deploy.Inputs = append(c.Deploy.Inputs, plan.NewStepInput(commandStepBuilder.Name(), plan.InputOptions{
Include: []string{"."},
}))
}
commandStepBuilder.Commands = plan.Spread(configStep.Commands, commandStepBuilder.Commands)
commandStepBuilder.Inputs = plan.Spread(configStep.Inputs, commandStepBuilder.Inputs)
commandStepBuilder.Secrets = plan.SpreadStrings(configStep.Secrets, commandStepBuilder.Secrets)
commandStepBuilder.Caches = plan.SpreadStrings(configStep.Caches, commandStepBuilder.Caches)
commandStepBuilder.AddEnvVars(configStep.Variables)
maps.Copy(commandStepBuilder.Assets, configStep.Assets)
}
// Update deploy from config
if c.Config.Deploy != nil {
if c.Config.Deploy.StartCmd != "" {
c.Deploy.StartCmd = c.Config.Deploy.StartCmd
}
c.Deploy.Inputs = plan.Spread(c.Config.Deploy.Inputs, c.Deploy.Inputs)
c.Deploy.Paths = plan.SpreadStrings(c.Config.Deploy.Paths, c.Deploy.Paths)
maps.Copy(c.Deploy.Variables, c.Config.Deploy.Variables)
}
}