mirror of
https://github.com/hanzoai/pack.git
synced 2026-09-30 23:43:13 +00:00
272 lines
8.4 KiB
Go
272 lines
8.4 KiB
Go
package core
|
|
|
|
import (
|
|
"strings"
|
|
|
|
"github.com/charmbracelet/log"
|
|
"github.com/railwayapp/railpack/core/app"
|
|
"github.com/railwayapp/railpack/core/config"
|
|
"github.com/railwayapp/railpack/core/generate"
|
|
"github.com/railwayapp/railpack/core/logger"
|
|
"github.com/railwayapp/railpack/core/plan"
|
|
"github.com/railwayapp/railpack/core/providers"
|
|
"github.com/railwayapp/railpack/core/providers/procfile"
|
|
"github.com/railwayapp/railpack/core/resolver"
|
|
"github.com/railwayapp/railpack/core/utils"
|
|
)
|
|
|
|
const (
|
|
defaultConfigFileName = "railpack.json"
|
|
)
|
|
|
|
type GenerateBuildPlanOptions struct {
|
|
RailpackVersion string
|
|
BuildCommand string
|
|
StartCommand string
|
|
PreviousVersions map[string]string
|
|
ConfigFilePath string
|
|
ErrorMissingStartCommand bool
|
|
}
|
|
|
|
type BuildResult struct {
|
|
RailpackVersion string `json:"railpackVersion,omitempty"`
|
|
Plan *plan.BuildPlan `json:"plan,omitempty"`
|
|
ResolvedPackages map[string]*resolver.ResolvedPackage `json:"resolvedPackages,omitempty"`
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
DetectedProviders []string `json:"detectedProviders,omitempty"`
|
|
Logs []logger.Msg `json:"logs,omitempty"`
|
|
Success bool `json:"success,omitempty"`
|
|
}
|
|
|
|
func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions) *BuildResult {
|
|
logger := logger.NewLogger()
|
|
|
|
// Get the full user config based on file config, env config, and options
|
|
config, err := GetConfig(app, env, options, logger)
|
|
if err != nil {
|
|
logger.LogError("%s", err.Error())
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
|
|
ctx, err := generate.NewGenerateContext(app, env, config, logger)
|
|
if err != nil {
|
|
logger.LogError("%s", err.Error())
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
|
|
// Set the preivous versions
|
|
if options.PreviousVersions != nil {
|
|
for name, version := range options.PreviousVersions {
|
|
ctx.Resolver.SetPreviousVersion(name, version)
|
|
}
|
|
}
|
|
|
|
// Figure out what providers to use
|
|
providerToUse, detectedProviderName := getProviders(ctx, config)
|
|
ctx.Metadata.Set("providers", detectedProviderName)
|
|
|
|
// TODO: We should indicate if we have packages specified in the config
|
|
// so that providers can determine if they should include mise in the final image (e.g. for shell script)
|
|
|
|
if providerToUse != nil {
|
|
err = providerToUse.Plan(ctx)
|
|
if err != nil {
|
|
logger.LogError("%s", err.Error())
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
}
|
|
|
|
// Run the procfile provider to support apps that have a Procfile with a start command
|
|
procfileProvider := &procfile.ProcfileProvider{}
|
|
if _, err := procfileProvider.Plan(ctx); err != nil {
|
|
logger.LogError("%s", err.Error())
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
|
|
buildPlan, resolvedPackages, err := ctx.Generate()
|
|
if err != nil {
|
|
logger.LogError("%s", err.Error())
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
|
|
if !ValidatePlan(buildPlan, app, logger, &ValidatePlanOptions{
|
|
ErrorMissingStartCommand: options.ErrorMissingStartCommand,
|
|
ProviderToUse: providerToUse,
|
|
}) {
|
|
return &BuildResult{Success: false, Logs: logger.Logs}
|
|
}
|
|
|
|
buildResult := &BuildResult{
|
|
RailpackVersion: options.RailpackVersion,
|
|
Plan: buildPlan,
|
|
ResolvedPackages: resolvedPackages,
|
|
Metadata: ctx.Metadata.Properties,
|
|
DetectedProviders: []string{detectedProviderName},
|
|
Logs: logger.Logs,
|
|
Success: true,
|
|
}
|
|
|
|
return buildResult
|
|
}
|
|
|
|
// GetConfig merges the options, environment, and file config into a single config
|
|
func GetConfig(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions, logger *logger.Logger) (*config.Config, error) {
|
|
optionsConfig := GenerateConfigFromOptions(options)
|
|
|
|
envConfig := GenerateConfigFromEnvironment(app, env)
|
|
|
|
fileConfig, err := GenerateConfigFromFile(app, env, options, logger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mergedConfig := config.Merge(optionsConfig, envConfig, fileConfig)
|
|
|
|
return mergedConfig, nil
|
|
}
|
|
|
|
// GenerateConfigFromFile generates a config from the config file
|
|
func GenerateConfigFromFile(app *app.App, env *app.Environment, options *GenerateBuildPlanOptions, logger *logger.Logger) (*config.Config, error) {
|
|
configFileName := defaultConfigFileName
|
|
if options.ConfigFilePath != "" {
|
|
configFileName = options.ConfigFilePath
|
|
}
|
|
|
|
if envConfigFileName, _ := env.GetConfigVariable("CONFIG_FILE"); envConfigFileName != "" {
|
|
configFileName = envConfigFileName
|
|
}
|
|
|
|
if !app.HasMatch(configFileName) {
|
|
if configFileName != defaultConfigFileName {
|
|
logger.LogWarn("Config file `%s` not found", configFileName)
|
|
}
|
|
|
|
return config.EmptyConfig(), nil
|
|
}
|
|
|
|
config := config.EmptyConfig()
|
|
if err := app.ReadJSON(configFileName, config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
logger.LogInfo("Using config file `%s`", configFileName)
|
|
logger.LogWarn("The config file format is not yet finalized and subject to change.")
|
|
|
|
return config, nil
|
|
}
|
|
|
|
// GenerateConfigFromEnvironment generates a config from the environment
|
|
func GenerateConfigFromEnvironment(app *app.App, env *app.Environment) *config.Config {
|
|
config := config.EmptyConfig()
|
|
|
|
if env == nil {
|
|
return config
|
|
}
|
|
|
|
if buildCmdVar, _ := env.GetConfigVariable("BUILD_CMD"); buildCmdVar != "" {
|
|
buildStep := config.GetOrCreateStep("build")
|
|
buildStep.Commands = []plan.Command{
|
|
plan.NewCopyCommand("."),
|
|
plan.NewExecShellCommand(buildCmdVar, plan.ExecOptions{CustomName: buildCmdVar}),
|
|
}
|
|
}
|
|
|
|
if startCmdVar, _ := env.GetConfigVariable("START_CMD"); startCmdVar != "" {
|
|
config.Deploy.StartCmd = startCmdVar
|
|
}
|
|
|
|
if envPackages, _ := env.GetConfigVariable("PACKAGES"); envPackages != "" {
|
|
config.Packages = utils.ParseVersions(strings.Split(envPackages, " "))
|
|
}
|
|
|
|
if envAptPackages, _ := env.GetConfigVariable("BUILD_APT_PACKAGES"); envAptPackages != "" {
|
|
config.BuildAptPackages = strings.Split(envAptPackages, " ")
|
|
}
|
|
|
|
if envAptPackages, _ := env.GetConfigVariable("DEPLOY_APT_PACKAGES"); envAptPackages != "" {
|
|
config.Deploy.AptPackages = strings.Split(envAptPackages, " ")
|
|
}
|
|
|
|
for name := range env.Variables {
|
|
config.Secrets = append(config.Secrets, name)
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
// GenerateConfigFromOptions generates a config from the CLI options
|
|
func GenerateConfigFromOptions(options *GenerateBuildPlanOptions) *config.Config {
|
|
config := config.EmptyConfig()
|
|
|
|
if options == nil {
|
|
return config
|
|
}
|
|
|
|
if options.BuildCommand != "" {
|
|
buildStep := config.GetOrCreateStep("build")
|
|
buildStep.Commands = []plan.Command{
|
|
plan.NewCopyCommand("."),
|
|
plan.NewExecShellCommand(options.BuildCommand, plan.ExecOptions{CustomName: options.BuildCommand}),
|
|
}
|
|
}
|
|
|
|
if options.StartCommand != "" {
|
|
config.Deploy.StartCmd = options.StartCommand
|
|
}
|
|
|
|
return config
|
|
}
|
|
|
|
func getProviders(ctx *generate.GenerateContext, config *config.Config) (providers.Provider, string) {
|
|
allProviders := providers.GetLanguageProviders()
|
|
|
|
var providerToUse providers.Provider
|
|
var detectedProvider string
|
|
|
|
// Even if there are providers manually specified, we want to detect to see what type of app this is
|
|
for _, provider := range allProviders {
|
|
matched, err := provider.Detect(ctx)
|
|
if err != nil {
|
|
log.Warnf("Failed to detect provider `%s`: %s", provider.Name(), err.Error())
|
|
continue
|
|
}
|
|
|
|
if matched {
|
|
detectedProvider = provider.Name()
|
|
|
|
// If there are no providers manually specified in the config,
|
|
if config.Provider == nil {
|
|
if err := provider.Initialize(ctx); err != nil {
|
|
ctx.Logger.LogWarn("Failed to initialize provider `%s`: %s", provider.Name(), err.Error())
|
|
continue
|
|
}
|
|
|
|
ctx.Logger.LogInfo("Detected %s", utils.CapitalizeFirst(provider.Name()))
|
|
|
|
providerToUse = provider
|
|
}
|
|
|
|
break
|
|
}
|
|
}
|
|
|
|
if config.Provider != nil {
|
|
provider := providers.GetProvider(*config.Provider)
|
|
|
|
if provider == nil {
|
|
ctx.Logger.LogWarn("Provider `%s` not found", *config.Provider)
|
|
return providerToUse, detectedProvider
|
|
}
|
|
|
|
if err := provider.Initialize(ctx); err != nil {
|
|
ctx.Logger.LogWarn("Failed to initialize provider `%s`: %s", *config.Provider, err.Error())
|
|
return providerToUse, detectedProvider
|
|
}
|
|
|
|
ctx.Logger.LogInfo("Using provider %s from config", utils.CapitalizeFirst(*config.Provider))
|
|
providerToUse = provider
|
|
}
|
|
|
|
return providerToUse, detectedProvider
|
|
}
|