diff --git a/.gitignore b/.gitignore index 1abaf8f..8babd35 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ # Go workspace file go.work +node_modules + # End of https://www.toptal.com/developers/gitignore/api/go bin diff --git a/buildkit/build.go b/buildkit/build.go index d16a511..25998ef 100644 --- a/buildkit/build.go +++ b/buildkit/build.go @@ -14,6 +14,8 @@ import ( _ "github.com/moby/buildkit/client/connhelper/dockercontainer" _ "github.com/moby/buildkit/client/connhelper/nerdctlcontainer" "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/session" + "github.com/moby/buildkit/session/secrets/secretsprovider" "github.com/moby/buildkit/util/appcontext" _ "github.com/moby/buildkit/util/grpcutil/encoding/proto" "github.com/moby/buildkit/util/progress/progressui" @@ -26,6 +28,8 @@ type BuildWithBuildkitClientOptions struct { DumpLLB bool OutputDir string ProgressMode string + SecretsHash string + Secrets map[string]string } func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWithBuildkitClientOptions) error { @@ -60,6 +64,7 @@ func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWith llbState, image, err := ConvertPlanToLLB(plan, ConvertPlanOptions{ BuildPlatform: buildPlatform, + SecretsHash: opts.SecretsHash, }) if err != nil { return fmt.Errorf("error converting plan to LLB: %w", err) @@ -142,10 +147,17 @@ func BuildWithBuildkitClient(appDir string, plan *plan.BuildPlan, opts BuildWith log.Debugf("Building image for %s with BuildKit %s", buildPlatform.String(), info.BuildkitVersion.Version) + secretsMap := make(map[string][]byte) + for k, v := range opts.Secrets { + secretsMap[k] = []byte(v) + } + secrets := secretsprovider.FromMap(secretsMap) + solveOpts := client.SolveOpt{ LocalMounts: map[string]fsutil.FS{ "context": appFS, }, + Session: []session.Attachable{secrets}, Exports: []client.ExportEntry{ { Type: client.ExporterDocker, diff --git a/buildkit/cache_store.go b/buildkit/cache_store.go index 4984a0f..afeb5f2 100644 --- a/buildkit/cache_store.go +++ b/buildkit/cache_store.go @@ -28,7 +28,7 @@ func NewBuildKitCacheStore(uniqueID string) *BuildKitCacheStore { func (c *BuildKitCacheStore) GetCache(key string, planCache *plan.Cache) BuildKitCache { cacheKey := key - if cacheKey == "" { + if c.uniqueID != "" { cacheKey = fmt.Sprintf("%s-%s", c.uniqueID, key) } diff --git a/buildkit/convert.go b/buildkit/convert.go index 056f7ef..f9891eb 100644 --- a/buildkit/convert.go +++ b/buildkit/convert.go @@ -12,6 +12,7 @@ import ( type ConvertPlanOptions struct { BuildPlatform BuildPlatform + SecretsHash string CacheKey string } @@ -24,14 +25,9 @@ func ConvertPlanToLLB(plan *p.BuildPlan, opts ConvertPlanOptions) (*llb.State, * state := getBaseState(platform) - // Add all variables as environment variables - for name, value := range plan.Variables { - state = state.AddEnv(name, value) - } - cacheStore := NewBuildKitCacheStore(opts.CacheKey) - graph, err := NewBuildGraph(plan, &state, cacheStore, &platform) + graph, err := NewBuildGraph(plan, &state, cacheStore, opts.SecretsHash, &platform) if err != nil { return nil, nil, err } @@ -124,10 +120,6 @@ func getImageEnv(graphOutput *BuildGraphOutput, plan *p.BuildPlan) []string { imageEnv = append(imageEnv, fmt.Sprintf("%s=%s", k, v)) } - for k, v := range plan.Start.Env { - imageEnv = append(imageEnv, fmt.Sprintf("%s=%s", k, v)) - } - return imageEnv } diff --git a/buildkit/frontend.go b/buildkit/frontend.go index 787fb0f..78bb7de 100644 --- a/buildkit/frontend.go +++ b/buildkit/frontend.go @@ -23,7 +23,11 @@ const ( configMountName = "dockerfile" // The default filename for the serialized Railpack plan - defaultRailpackPlan = "rpk.json" + defaultRailpackPlan = "railpack-plan.json" + + secretsHash = "secrets-hash" + + cacheKey = "cache-key" ) func StartFrontend() { @@ -37,7 +41,11 @@ func StartFrontend() { } func Build(ctx context.Context, c client.Client) (*client.Result, error) { - buildPlatform, err := validatePlatform(c.BuildOpts().Opts) + opts := c.BuildOpts().Opts + cacheKey := opts[cacheKey] + secretsHash := opts[secretsHash] + + buildPlatform, err := validatePlatform(opts) if err != nil { return nil, err } @@ -47,8 +55,15 @@ func Build(ctx context.Context, c client.Client) (*client.Result, error) { return nil, err } + _, err = json.MarshalIndent(plan, "", " ") + if err != nil { + return nil, fmt.Errorf("error marshalling plan: %w", err) + } + llbState, image, err := ConvertPlanToLLB(plan, ConvertPlanOptions{ BuildPlatform: buildPlatform, + SecretsHash: secretsHash, + CacheKey: cacheKey, }) if err != nil { return nil, fmt.Errorf("error converting plan to LLB: %w", err) diff --git a/buildkit/graph.go b/buildkit/graph.go index d1733f4..2098a6c 100644 --- a/buildkit/graph.go +++ b/buildkit/graph.go @@ -13,11 +13,12 @@ import ( ) type BuildGraph struct { - Nodes map[string]*Node - BaseState *llb.State - CacheStore *BuildKitCacheStore - Plan *plan.BuildPlan - Platform *specs.Platform + Nodes map[string]*Node + BaseState *llb.State + CacheStore *BuildKitCacheStore + SecretsHash string + Plan *plan.BuildPlan + Platform *specs.Platform } type BuildGraphOutput struct { @@ -26,13 +27,14 @@ type BuildGraphOutput struct { EnvVars map[string]string } -func NewBuildGraph(plan *plan.BuildPlan, baseState *llb.State, cacheStore *BuildKitCacheStore, platform *specs.Platform) (*BuildGraph, error) { +func NewBuildGraph(plan *plan.BuildPlan, baseState *llb.State, cacheStore *BuildKitCacheStore, secretsHash string, platform *specs.Platform) (*BuildGraph, error) { graph := &BuildGraph{ - Nodes: make(map[string]*Node), - BaseState: baseState, - CacheStore: cacheStore, - Plan: plan, - Platform: platform, + Nodes: make(map[string]*Node), + BaseState: baseState, + CacheStore: cacheStore, + SecretsHash: secretsHash, + Plan: plan, + Platform: platform, } // Create a node for each step @@ -268,18 +270,20 @@ func (g *BuildGraph) convertStepToLLB(node *Node, baseState *llb.State) (*llb.St } // Process the step commands - for _, cmd := range step.Commands { - var err error - state, err = g.convertCommandToLLB(node, cmd, state, step) - if err != nil { - return nil, err + if step.Commands != nil { + for _, cmd := range *step.Commands { + var err error + state, err = g.convertCommandToLLB(node, cmd, state, step) + if err != nil { + return nil, err + } } } - if len(step.Outputs) > 0 { + if step.Outputs != nil { result := llb.Scratch() - for _, output := range step.Outputs { + for _, output := range *step.Outputs { result = result.File(llb.Copy(state, output, output, &llb.CopyInfo{ CreateDestPath: true, AllowWildcard: true, @@ -311,6 +315,18 @@ func (g *BuildGraph) convertCommandToLLB(node *Node, cmd plan.Command, state llb opts = append(opts, llb.WithCustomName(cmd.CustomName)) } + if node.Step.UseSecrets == nil || *node.Step.UseSecrets { // default to using secrets + for _, secret := range g.Plan.Secrets { + opts = append(opts, llb.AddSecret(secret, llb.SecretID(secret), llb.SecretAsEnv(true), llb.SecretAsEnvName(secret))) + } + + // If there is a secrets hash, add a mount to invalidate the cache if the secrets hash changes + if g.SecretsHash != "" { + opts = append(opts, llb.AddMount("/cache-invalidate", + llb.Scratch().File(llb.Mkfile("secrets-hash", 0644, []byte(g.SecretsHash))))) + } + } + if len(cmd.Caches) > 0 { cacheOpts, err := g.getCacheMountOptions(cmd.Caches) if err != nil { @@ -526,6 +542,8 @@ func (g *BuildGraph) getCacheMountOptions(cacheKeys []string) ([]llb.RunOption, opts = append(opts, llb.AddMount(planCache.Directory, *cache.cacheState, llb.AsPersistentCacheDir(cache.cacheKey, cacheType)), ) + + return opts, nil } else { return nil, fmt.Errorf("cache with key %q not found", cacheKey) } diff --git a/bun.lockb b/bun.lockb new file mode 100755 index 0000000..91397bd Binary files /dev/null and b/bun.lockb differ diff --git a/cli/build.go b/cli/build.go index 16ec07e..a369617 100644 --- a/cli/build.go +++ b/cli/build.go @@ -2,11 +2,15 @@ package cli import ( "context" + "crypto/sha256" "encoding/json" + "fmt" "github.com/charmbracelet/log" "github.com/railwayapp/railpack/buildkit" "github.com/railwayapp/railpack/core" + "github.com/railwayapp/railpack/core/app" + "github.com/railwayapp/railpack/core/plan" "github.com/urfave/cli/v3" ) @@ -53,7 +57,7 @@ var BuildCommand = &cli.Command{ }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - buildResult, app, err := GenerateBuildResultForCommand(cmd) + buildResult, app, env, err := GenerateBuildResultForCommand(cmd) if err != nil { return cli.Exit(err, 1) } @@ -69,11 +73,20 @@ var BuildCommand = &cli.Command{ log.Debug(string(serializedPlan)) } + err = validateSecrets(buildResult.Plan, env) + if err != nil { + return cli.Exit(err, 1) + } + + secretsHash := getSecretsHash(env) + err = buildkit.BuildWithBuildkitClient(app.Source, buildResult.Plan, buildkit.BuildWithBuildkitClientOptions{ ImageName: cmd.String("name"), DumpLLB: cmd.Bool("llb"), OutputDir: cmd.String("output"), ProgressMode: cmd.String("progress"), + SecretsHash: secretsHash, + Secrets: env.Variables, }) if err != nil { return cli.Exit(err, 1) @@ -82,3 +95,22 @@ var BuildCommand = &cli.Command{ return nil }, } + +func validateSecrets(plan *plan.BuildPlan, env *app.Environment) error { + for _, secret := range plan.Secrets { + if _, ok := env.Variables[secret]; !ok { + return fmt.Errorf("missing environment variable: %s. Please set the envvar with --env %s=%s", secret, secret, "...") + } + } + return nil +} + +func getSecretsHash(env *app.Environment) string { + secretsValue := "" + for _, v := range env.Variables { + secretsValue += v + } + hasher := sha256.New() + hasher.Write([]byte(secretsValue)) + return fmt.Sprintf("%x", hasher.Sum(nil)) +} diff --git a/cli/common.go b/cli/common.go index 4f48511..f56f51c 100644 --- a/cli/common.go +++ b/cli/common.go @@ -9,16 +9,16 @@ import ( "github.com/urfave/cli/v3" ) -func GenerateBuildResultForCommand(cmd *cli.Command) (*core.BuildResult, *a.App, error) { +func GenerateBuildResultForCommand(cmd *cli.Command) (*core.BuildResult, *a.App, *a.Environment, error) { directory := cmd.Args().First() if directory == "" { - return nil, nil, cli.Exit("directory argument is required", 1) + return nil, nil, nil, cli.Exit("directory argument is required", 1) } app, err := a.NewApp(directory) if err != nil { - return nil, nil, fmt.Errorf("error creating app: %w", err) + return nil, nil, nil, fmt.Errorf("error creating app: %w", err) } log.Debugf("Building %s", app.Source) @@ -27,7 +27,7 @@ func GenerateBuildResultForCommand(cmd *cli.Command) (*core.BuildResult, *a.App, env, err := a.FromEnvs(envsArgs) if err != nil { - return nil, nil, fmt.Errorf("error creating env: %w", err) + return nil, nil, nil, fmt.Errorf("error creating env: %w", err) } generateOptions := &core.GenerateBuildPlanOptions{ @@ -37,8 +37,8 @@ func GenerateBuildResultForCommand(cmd *cli.Command) (*core.BuildResult, *a.App, buildResult, err := core.GenerateBuildPlan(app, env, generateOptions) if err != nil { - return nil, nil, fmt.Errorf("error generating build plan: %w", err) + return nil, nil, nil, fmt.Errorf("error generating build plan: %w", err) } - return buildResult, app, nil + return buildResult, app, env, nil } diff --git a/cli/info.go b/cli/info.go index e8871b8..7d7c45c 100644 --- a/cli/info.go +++ b/cli/info.go @@ -37,7 +37,7 @@ var InfoCommand = &cli.Command{ }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - buildResult, _, err := GenerateBuildResultForCommand(cmd) + buildResult, _, _, err := GenerateBuildResultForCommand(cmd) if err != nil { return cli.Exit(err, 1) } diff --git a/cli/plan.go b/cli/plan.go index 8c88052..5441d42 100644 --- a/cli/plan.go +++ b/cli/plan.go @@ -34,7 +34,7 @@ var PlanCommand = &cli.Command{ }, }, Action: func(ctx context.Context, cmd *cli.Command) error { - buildResult, _, err := GenerateBuildResultForCommand(cmd) + buildResult, _, _, err := GenerateBuildResultForCommand(cmd) if err != nil { return cli.Exit(err, 1) } diff --git a/core/config/config.go b/core/config/config.go index 3cc5193..e5c946d 100644 --- a/core/config/config.go +++ b/core/config/config.go @@ -27,6 +27,9 @@ type Config struct { // Map of cache name to cache definitions. The cache key can be referenced in an exec command. Caches map[string]*plan.Cache `json:"caches,omitempty" jsonschema:"description=Map of cache name to cache definitions. The cache key can be referenced in an exec command"` + + // Secrets that should be made available to commands that have useSecrets set to true + Secrets []string `json:"secrets,omitempty" jsonschema:"description=Secrets that should be made available to commands that have useSecrets set to true"` } func EmptyConfig() *Config { @@ -54,37 +57,12 @@ func Merge(configs ...*Config) *Config { } result := EmptyConfig() - for _, config := range configs { if config == nil { continue } - // Strings (use last non-empty value) - if config.BaseImage != "" { - result.BaseImage = config.BaseImage - } - - if config.Start.Command != "" { - result.Start = config.Start - } - - // Maps (overwrite existing values) - for k, v := range config.Caches { - result.Caches[k] = v - } - for k, v := range config.Packages { - result.Packages[k] = v - } - for k, v := range config.Steps { - result.Steps[k] = v - } - - // Arrays (extend) - result.AptPackages = append(result.AptPackages, config.AptPackages...) - - // Merge providers - result.Providers = utils.MergeStringSlicePointers(result.Providers, config.Providers) + utils.MergeStructs(result, config) } return result diff --git a/core/config/config_test.go b/core/config/config_test.go index 1e1e8d3..2975faf 100644 --- a/core/config/config_test.go +++ b/core/config/config_test.go @@ -4,7 +4,7 @@ import ( "encoding/json" "testing" - "github.com/railwayapp/railpack/core/plan" + "github.com/google/go-cmp/cmp" "github.com/stretchr/testify/require" ) @@ -17,72 +17,260 @@ func TestEmptyConfig(t *testing.T) { require.Empty(t, config.Steps) } +func TestMergeConfigSmall(t *testing.T) { + config1JSON := `{ + "baseImage": "ubuntu:20.04", + "packages": { + "python": "latest", + "node": "22" + }, + "aptPackages": ["git"], + "steps": { + "install": { + "dependsOn": ["packages"], + "commands": [ + "echo first" + ] + } + } + }` + + config2JSON := `{ + "baseImage": "secondd", + "packages": { + "node": "23", + "bun": "latest" + }, + "steps": { + "install": {} + } + }` + + expectedJSON := `{ + "baseImage": "secondd", + "packages": { + "python": "latest", + "node": "23", + "bun": "latest" + }, + "aptPackages": ["git"], + "steps": { + "install": { + "dependsOn": ["packages"], + "commands": [ + "echo first" + ] + } + }, + "caches": {} + }` + + var config1, config2, expected Config + require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1)) + require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2)) + require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected)) + + result := Merge(&config1, &config2) + + if diff := cmp.Diff(expected, *result); diff != "" { + t.Errorf("configs mismatch (-want +got):\n%s", diff) + } + +} + func TestMergeConfig(t *testing.T) { - config1 := &Config{ - BaseImage: "ubuntu:20.04", - Packages: map[string]string{ + config1JSON := `{ + "baseImage": "ubuntu:20.04", + "packages": { "python": "latest", - "node": "22", + "node": "22" }, - AptPackages: []string{"git"}, - Steps: map[string]*plan.Step{ + "aptPackages": ["git"], + "caches": { + "npm": { + "directory": "/root/.npm", + "type": "locked" + }, + "pip": { + "directory": "/root/.cache/pip" + } + }, + "secrets": ["SECRET_1", "API_KEY"], + "steps": { "install": { - Name: "install", - Commands: []plan.Command{ - plan.NewExecCommand("npm install"), + "name": "install", + "useSecrets": true, + "outputs": ["node_modules/", "package-lock.json"], + "assets": { + "package.json": "content1", + "requirements.txt": "content2" }, + "startingImage": "node:16", + "commands": [ + {"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"}, + {"type": "path", "path": "/usr/local/bin"}, + {"type": "variable", "name": "NODE_ENV", "value": "production"}, + {"type": "copy", "src": "/src", "dest": "/app", "image": "alpine:latest"}, + {"type": "file", "path": "/app", "name": "config.json", "mode": 384, "customName": "Write config"} + ] }, "build": { - Name: "build", - Commands: []plan.Command{ - plan.NewExecCommand("config 1 a"), - plan.NewExecCommand("config 1 b"), - }, + "name": "build", + "commands": [ + {"type": "exec", "cmd": "config 1 a"}, + {"type": "exec", "cmd": "config 1 b"} + ] + } + }, + "start": { + "command": "python app.py" + } + }` + + config2JSON := `{ + "providers": ["node"], + "baseImage": "ubuntu:22.04", + "packages": { + "node": "23" + }, + "aptPackages": ["curl"], + "caches": { + "npm": { + "directory": "/root/.npm-new", + "type": "shared" }, + "go": { + "directory": "/root/.cache/go-build" + } }, - Start: plan.Start{ - Command: "python app.py", + "secrets": ["SECRET_2"], + "steps": { + "install": { + "name": "install", + "useSecrets": true, + "outputs": ["dist/"], + "assets": { + "package.json": "content3" + }, + "startingImage": "node:18" + }, + "build": { + "name": "build", + "useSecrets": false, + "commands": [ + {"type": "exec", "cmd": "config 2 a"}, + {"type": "exec", "cmd": "config 2 b"} + ] + } }, - } + "start": { + "baseImage": "node:18", + "command": "node server.js", + "paths": ["/usr/local/bin", "/app/bin"] + } + }` - config2 := &Config{ - Providers: &[]string{"node"}, - BaseImage: "ubuntu:22.04", - Packages: map[string]string{ - "node": "23", + expectedJSON := `{ + "providers": ["node"], + "baseImage": "ubuntu:22.04", + "packages": { + "python": "latest", + "node": "23" }, - AptPackages: []string{"curl"}, - Steps: map[string]*plan.Step{ - "build": { - Name: "build", - Commands: []plan.Command{ - plan.NewExecCommand("config 2 a"), - plan.NewExecCommand("config 2 b"), - }, + "aptPackages": ["curl"], + "caches": { + "npm": { + "directory": "/root/.npm-new", + "type": "shared" + }, + "go": { + "directory": "/root/.cache/go-build" }, + "pip": { + "directory": "/root/.cache/pip" + } }, - Start: plan.Start{ - Command: "node server.js", + "secrets": ["SECRET_2"], + "steps": { + "install": { + "name": "install", + "useSecrets": true, + "outputs": ["dist/"], + "assets": { + "package.json": "content3", + "requirements.txt": "content2" + }, + "startingImage": "node:18", + "commands": [ + {"type": "exec", "cmd": "npm install", "caches": ["npm"], "customName": "Install NPM deps"}, + {"type": "path", "path": "/usr/local/bin"}, + {"type": "variable", "name": "NODE_ENV", "value": "production"}, + {"type": "copy", "src": "/src", "dest": "/app", "image": "alpine:latest"}, + {"type": "file", "path": "/app", "name": "config.json", "mode": 384, "customName": "Write config"} + ] + }, + "build": { + "name": "build", + "useSecrets": false, + "commands": [ + {"type": "exec", "cmd": "config 2 a"}, + {"type": "exec", "cmd": "config 2 b"} + ] + } }, + "start": { + "baseImage": "node:18", + "command": "node server.js", + "paths": ["/usr/local/bin", "/app/bin"] + } + }` + + var config1, config2, expected Config + require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1)) + require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2)) + require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected)) + + result := Merge(&config1, &config2) + + if diff := cmp.Diff(expected, *result); diff != "" { + t.Errorf("configs mismatch (-want +got):\n%s", diff) } +} + +func TestMergeConfigStart(t *testing.T) { + config1JSON := `{ + "start": { + "command": "python app.py" + } + }` + + config2JSON := `{ + "packages": { + "node": "23" + } + }` - result := Merge(config1, config2) - - require.Equal(t, &[]string{"node"}, result.Providers) - require.Equal(t, "ubuntu:22.04", result.BaseImage) - require.Equal(t, "latest", result.Packages["python"]) - require.Equal(t, "23", result.Packages["node"]) - require.ElementsMatch(t, []string{"git", "curl"}, result.AptPackages) - require.Equal(t, "node server.js", result.Start.Command) - require.Len(t, result.Steps["install"].Commands, 1) - require.Equal(t, "npm install", result.Steps["install"].Commands[0].(plan.ExecCommand).Cmd) - require.Len(t, result.Steps["build"].Commands, 2) - require.Equal(t, "config 2 a", result.Steps["build"].Commands[0].(plan.ExecCommand).Cmd) - require.Equal(t, "config 2 b", result.Steps["build"].Commands[1].(plan.ExecCommand).Cmd) - - config2.Start = plan.Start{} - result = Merge(config1, config2) - require.Equal(t, "python app.py", result.Start.Command) + expectedJSON := `{ + "packages": { + "node": "23" + }, + "start": { + "command": "python app.py" + }, + "steps": {}, + "caches": {} + }` + + var config1, config2, expected Config + require.NoError(t, json.Unmarshal([]byte(config1JSON), &config1)) + require.NoError(t, json.Unmarshal([]byte(config2JSON), &config2)) + require.NoError(t, json.Unmarshal([]byte(expectedJSON), &expected)) + + result := Merge(&config1, &config2) + + if diff := cmp.Diff(expected, *result); diff != "" { + t.Errorf("configs mismatch (-want +got):\n%s", diff) + } } func TestGetJsonSchema(t *testing.T) { diff --git a/core/core.go b/core/core.go index 8f4a8f7..dd1ecab 100644 --- a/core/core.go +++ b/core/core.go @@ -12,7 +12,6 @@ import ( "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 ( @@ -78,39 +77,11 @@ func GenerateBuildPlan(app *app.App, env *app.Environment, options *GenerateBuil return nil, fmt.Errorf("failed to apply config: %w", err) } - // Resolve all package versions into a fully qualified and valid version - resolvedPackages, err := ctx.ResolvePackages() + buildPlan, resolvedPackages, err := ctx.Generate() if err != nil { - return nil, fmt.Errorf("failed to resolve packages: %w", err) + return nil, fmt.Errorf("failed to generate build plan: %w", err) } - // Generate the plan based on the context and resolved packages - - buildPlan := plan.NewBuildPlan() - - buildStepOptions := &generate.BuildStepOptions{ - ResolvedPackages: resolvedPackages, - Caches: ctx.Caches, - } - - buildPlan.Variables = ctx.Variables - for _, stepBuilder := range ctx.Steps { - step, err := stepBuilder.Build(buildStepOptions) - - if err != nil { - return nil, fmt.Errorf("failed to build step: %w", err) - } - - buildPlan.AddStep(*step) - } - - buildPlan.Caches = ctx.Caches.Caches - - buildPlan.Start.BaseImage = ctx.Start.BaseImage - buildPlan.Start.Command = ctx.Start.Command - buildPlan.Start.Paths = utils.RemoveDuplicates(ctx.Start.Paths) - buildPlan.Start.Env = ctx.Start.Env - buildResult := &BuildResult{ Plan: buildPlan, ResolvedPackages: resolvedPackages, @@ -165,13 +136,13 @@ func GenerateConfigFromEnvironment(app *app.App, env *app.Environment) *config.C if installCmdVar, _ := env.GetConfigVariable("INSTALL_CMD"); installCmdVar != "" { installStep := config.GetOrCreateStep("install") - installStep.Commands = []plan.Command{plan.NewExecCommand(installCmdVar)} + installStep.Commands = &[]plan.Command{plan.NewExecCommand(installCmdVar)} installStep.DependsOn = append(installStep.DependsOn, "packages") } if buildCmdVar, _ := env.GetConfigVariable("BUILD_CMD"); buildCmdVar != "" { buildStep := config.GetOrCreateStep("build") - buildStep.Commands = []plan.Command{plan.NewExecCommand(buildCmdVar)} + buildStep.Commands = &[]plan.Command{plan.NewExecCommand(buildCmdVar)} buildStep.DependsOn = append(buildStep.DependsOn, "install") } @@ -190,6 +161,10 @@ func GenerateConfigFromEnvironment(app *app.App, env *app.Environment) *config.C config.AptPackages = strings.Split(envAptPackages, " ") } + for name := range env.Variables { + config.Secrets = append(config.Secrets, name) + } + return config } @@ -203,7 +178,7 @@ func GenerateConfigFromOptions(options *GenerateBuildPlanOptions) *config.Config if options.BuildCommand != "" { buildStep := config.GetOrCreateStep("build") - buildStep.Commands = []plan.Command{plan.NewExecCommand(options.BuildCommand)} + buildStep.Commands = &[]plan.Command{plan.NewExecCommand(options.BuildCommand)} buildStep.DependsOn = append(buildStep.DependsOn, "install") } diff --git a/core/generate/__snapshots__/context_test.snap b/core/generate/__snapshots__/context_test.snap new file mode 100755 index 0000000..ed6af24 --- /dev/null +++ b/core/generate/__snapshots__/context_test.snap @@ -0,0 +1,149 @@ + +[TestGenerateContext - 1] +{ + "caches": { + "apt": { + "directory": "/var/cache/apt", + "type": "locked" + }, + "apt-lists": { + "directory": "/var/lib/apt/lists", + "type": "locked" + }, + "mise": { + "directory": "/mise/cache", + "type": "shared" + } + }, + "secrets": [ + "RAILWAY_SECRET_1", + "RAILWAY_SECRET_2" + ], + "start": { + "cmd": "npm run start" + }, + "steps": [ + { + "assets": { + "mise.toml": "[tools]\n [tools.node]\n version = \"20.18.2\"\n [tools.python]\n version = \"3.11.11\"\n" + }, + "commands": [ + { + "name": "MISE_DATA_DIR", + "value": "/mise" + }, + { + "name": "MISE_CONFIG_DIR", + "value": "/mise" + }, + { + "name": "MISE_INSTALL_PATH", + "value": "/usr/local/bin/mise" + }, + { + "name": "MISE_CACHE_DIR", + "value": "/mise/cache" + }, + { + "path": "/mise/shims" + }, + { + "caches": [ + "apt", + "apt-lists" + ], + "cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y ca-certificates curl'", + "customName": "install apt packages: ca-certificates curl" + }, + { + "caches": [ + "mise" + ], + "cmd": "sh -c 'curl -fsSL https://mise.run | sh'", + "customName": "install mise" + }, + { + "customName": "create mise config", + "name": "mise.toml", + "path": "/etc/mise/config.toml" + }, + { + "caches": [ + "mise" + ], + "cmd": "sh -c 'mise trust -a \u0026\u0026 mise install'", + "customName": "install mise packages: node, python" + } + ], + "dependsOn": [ + "packages:apt:config" + ], + "name": "packages:mise", + "outputs": [ + "/mise/shims", + "/mise/installs", + "/usr/local/bin/mise", + "/etc/mise/config.toml", + "/root/.local/state/mise" + ], + "useSecrets": false + }, + { + "commands": [ + { + "caches": [ + "apt", + "apt-lists" + ], + "cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y git neofetch'", + "customName": "install apt packages: git neofetch" + } + ], + "name": "packages:apt:test", + "useSecrets": false + }, + { + "commands": [ + { + "cmd": "npm install" + } + ], + "dependsOn": [ + "packages:apt:test" + ], + "name": "install", + "outputs": [ + "node_modules" + ] + }, + { + "commands": [ + { + "cmd": "npm run build" + }, + { + "cmd": "echo building" + } + ], + "dependsOn": [ + "install" + ], + "name": "build" + }, + { + "commands": [ + { + "caches": [ + "apt", + "apt-lists" + ], + "cmd": "sh -c 'apt-get update \u0026\u0026 apt-get install -y curl'", + "customName": "install apt packages: curl" + } + ], + "name": "packages:apt:config", + "useSecrets": false + } + ] +} +--- diff --git a/core/generate/apt_step_builder.go b/core/generate/apt_step_builder.go index 64621f5..010986c 100644 --- a/core/generate/apt_step_builder.go +++ b/core/generate/apt_step_builder.go @@ -38,8 +38,10 @@ func (b *AptStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) { step.DependsOn = utils.RemoveDuplicates(b.DependsOn) step.AddCommands([]plan.Command{ - options.NewAptInstallCommand(utils.RemoveDuplicates(b.Packages)), + options.NewAptInstallCommand(b.Packages), }) + step.UseSecrets = &[]bool{false}[0] + return step, nil } diff --git a/core/generate/command_step_builder.go b/core/generate/command_step_builder.go index 32faa7d..4157108 100644 --- a/core/generate/command_step_builder.go +++ b/core/generate/command_step_builder.go @@ -8,17 +8,17 @@ import ( type CommandStepBuilder struct { DisplayName string DependsOn []string - Commands []plan.Command - Outputs []string + Commands *[]plan.Command + Outputs *[]string Assets map[string]string + UseSecrets *bool } func (c *GenerateContext) NewCommandStep(name string) *CommandStepBuilder { step := &CommandStepBuilder{ DisplayName: c.GetStepName(name), DependsOn: []string{MisePackageStepName}, - Commands: []plan.Command{}, - Outputs: []string{}, + Commands: &[]plan.Command{}, Assets: map[string]string{}, } @@ -32,11 +32,17 @@ func (b *CommandStepBuilder) DependOn(name string) { } func (b *CommandStepBuilder) AddCommand(command plan.Command) { - b.Commands = append(b.Commands, command) + if b.Commands == nil { + b.Commands = &[]plan.Command{} + } + *b.Commands = append(*b.Commands, command) } func (b *CommandStepBuilder) AddCommands(commands []plan.Command) { - b.Commands = append(b.Commands, commands...) + if b.Commands == nil { + b.Commands = &[]plan.Command{} + } + *b.Commands = append(*b.Commands, commands...) } func (b *CommandStepBuilder) Name() string { @@ -47,9 +53,10 @@ func (b *CommandStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error step := plan.NewStep(b.DisplayName) step.DependsOn = utils.RemoveDuplicates(b.DependsOn) - step.Outputs = utils.RemoveDuplicates(b.Outputs) + step.Outputs = b.Outputs step.Commands = b.Commands step.Assets = b.Assets + step.UseSecrets = b.UseSecrets return step, nil } diff --git a/core/generate/context.go b/core/generate/context.go index 246c45c..2f7f18b 100644 --- a/core/generate/context.go +++ b/core/generate/context.go @@ -1,6 +1,8 @@ package generate import ( + "fmt" + "sort" "strings" "github.com/charmbracelet/log" @@ -9,6 +11,7 @@ import ( "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 { @@ -28,8 +31,8 @@ type GenerateContext struct { Steps []StepBuilder Start StartContext - Caches *CacheContext - Variables map[string]string + Caches *CacheContext + Secrets []string SubContexts []string @@ -46,14 +49,14 @@ func NewGenerateContext(app *a.App, env *a.Environment) (*GenerateContext, error } return &GenerateContext{ - App: app, - Env: env, - Variables: map[string]string{}, - Steps: make([]StepBuilder, 0), - Start: *NewStartContext(), - Caches: NewCacheContext(), - Metadata: NewMetadata(), - resolver: resolver, + App: app, + Env: env, + Steps: make([]StepBuilder, 0), + Start: *NewStartContext(), + Caches: NewCacheContext(), + Secrets: []string{}, + Metadata: NewMetadata(), + resolver: resolver, }, nil } @@ -95,6 +98,44 @@ func (c *GenerateContext) ResolvePackages() (map[string]*resolver.ResolvedPackag return c.resolver.ResolvePackages() } +// Generate a build plan from the context +func (c *GenerateContext) Generate() (*plan.BuildPlan, map[string]*resolver.ResolvedPackage, error) { + // Resolve all package versions into a fully qualified and valid version + resolvedPackages, err := c.ResolvePackages() + if err != nil { + return nil, nil, fmt.Errorf("failed to resolve packages: %w", err) + } + + // Generate the plan based on the context and resolved packages + + 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.Start.BaseImage = c.Start.BaseImage + buildPlan.Start.Command = c.Start.Command + buildPlan.Start.Paths = utils.RemoveDuplicates(c.Start.Paths) + + return buildPlan, resolvedPackages, nil +} + func (c *GenerateContext) ApplyConfig(config *config.Config) error { // Mise package config miseStep := c.GetMiseStepBuilder() @@ -132,15 +173,17 @@ func (c *GenerateContext) ApplyConfig(config *config.Config) error { if len(configStep.DependsOn) > 0 { commandStepBuilder.DependsOn = configStep.DependsOn } - if len(configStep.Commands) > 0 { - commandStepBuilder.Commands = configStep.Commands + if configStep.Commands != nil { + commandStepBuilder.AddCommands(*configStep.Commands) } - if len(configStep.Outputs) > 0 { + if configStep.Outputs != nil { commandStepBuilder.Outputs = configStep.Outputs } for k, v := range configStep.Assets { commandStepBuilder.Assets[k] = v } + + commandStepBuilder.UseSecrets = configStep.UseSecrets } // Cache config @@ -148,6 +191,9 @@ func (c *GenerateContext) ApplyConfig(config *config.Config) error { c.Caches.SetCache(name, cache) } + // Secret config + c.Secrets = append(c.Secrets, config.Secrets...) + // Start config if config.Start.BaseImage != "" { c.Start.BaseImage = config.Start.BaseImage @@ -161,19 +207,13 @@ func (c *GenerateContext) ApplyConfig(config *config.Config) error { c.Start.Paths = append(c.Start.Paths, config.Start.Paths...) } - if len(config.Start.Env) > 0 { - if c.Start.Env == nil { - c.Start.Env = make(map[string]string) - } - for k, v := range config.Start.Env { - c.Start.Env[k] = v - } - } - return nil } 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, " "), Caches: o.Caches.GetAptCaches(), diff --git a/core/generate/context_test.go b/core/generate/context_test.go new file mode 100644 index 0000000..c655e52 --- /dev/null +++ b/core/generate/context_test.go @@ -0,0 +1,111 @@ +package generate + +import ( + "encoding/json" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" + "github.com/railwayapp/railpack/core/app" + "github.com/railwayapp/railpack/core/config" + "github.com/railwayapp/railpack/core/plan" + "github.com/stretchr/testify/require" +) + +type TestProvider struct{} + +func (p *TestProvider) Plan(ctx *GenerateContext) error { + // mise + mise := ctx.GetMiseStepBuilder() + nodeRef := mise.Default("node", "18") + mise.Version(nodeRef, "18", "test") + + // apt + aptStep := ctx.NewAptStepBuilder("test") + aptStep.AddAptPackage("git") + aptStep.AddAptPackage("neofetch") + + // commands + installStep := ctx.NewCommandStep("install") + installStep.AddCommand(plan.NewExecCommand("npm install", plan.ExecOptions{})) + installStep.Outputs = &[]string{"node_modules"} + installStep.DependsOn = []string{aptStep.Name()} + + buildStep := ctx.NewCommandStep("build") + buildStep.AddCommand(plan.NewExecCommand("npm run build", plan.ExecOptions{})) + buildStep.DependsOn = []string{installStep.Name()} + + ctx.Start.Command = "npm run start" + + return nil +} + +func CreateTestContext(t *testing.T, path string) *GenerateContext { + t.Helper() + + userApp, err := app.NewApp(path) + require.NoError(t, err) + + env := app.NewEnvironment(nil) + + ctx, err := NewGenerateContext(userApp, env) + require.NoError(t, err) + + return ctx +} + +func TestGenerateContext(t *testing.T) { + ctx := CreateTestContext(t, "../../examples/node-npm") + provider := &TestProvider{} + require.NoError(t, provider.Plan(ctx)) + + // User defined config + configJSON := `{ + "packages": { + "node": "20", + "python": "3.11" + }, + "aptPackages": ["curl"], + "steps": { + "build": { + "commands": ["echo building"] + } + }, + "secrets": ["RAILWAY_SECRET_1", "RAILWAY_SECRET_2"] + }` + + var cfg config.Config + require.NoError(t, json.Unmarshal([]byte(configJSON), &cfg)) + + // Apply the config to the context + require.NoError(t, ctx.ApplyConfig(&cfg)) + + // Resolve packages + resolvedPkgs, err := ctx.ResolvePackages() + require.NoError(t, err) + + // Generate a plan + buildOpts := &BuildStepOptions{ + ResolvedPackages: resolvedPkgs, + Caches: ctx.Caches, + } + + // Build and verify each step + for _, builder := range ctx.Steps { + _, err := builder.Build(buildOpts) + require.NoError(t, err) + } + + buildPlan, _, err := ctx.Generate() + require.NoError(t, err) + + buildPlanJSON, err := json.Marshal(buildPlan) + require.NoError(t, err) + + var actualPlan map[string]interface{} + require.NoError(t, json.Unmarshal(buildPlanJSON, &actualPlan)) + + serializedPlan, err := json.MarshalIndent(actualPlan, "", " ") + require.NoError(t, err) + + snaps.MatchJSON(t, serializedPlan) +} diff --git a/core/generate/image_step_builder.go b/core/generate/image_step_builder.go index 277f2ef..04d9cc1 100644 --- a/core/generate/image_step_builder.go +++ b/core/generate/image_step_builder.go @@ -6,11 +6,10 @@ import ( ) type ImageStepBuilder struct { - DisplayName string - Resolver *resolver.Resolver - Packages []*resolver.PackageRef - Outputs []string - + DisplayName string + Resolver *resolver.Resolver + Packages []*resolver.PackageRef + Outputs *[]string ResolveStepImage func(options *BuildStepOptions) string } @@ -51,6 +50,7 @@ func (b *ImageStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) step.StartingImage = b.ResolveStepImage(options) step.Outputs = b.Outputs + step.UseSecrets = &[]bool{false}[0] return step, nil } diff --git a/core/generate/mise_step_builder.go b/core/generate/mise_step_builder.go index c7b6d7d..124f61e 100644 --- a/core/generate/mise_step_builder.go +++ b/core/generate/mise_step_builder.go @@ -2,6 +2,7 @@ package generate import ( "fmt" + "sort" "strings" a "github.com/railwayapp/railpack/core/app" @@ -22,7 +23,7 @@ type MiseStepBuilder struct { SupportingMiseFiles []string Assets map[string]string DependsOn []string - Outputs []string + Outputs *[]string app *a.App env *a.Environment @@ -36,7 +37,7 @@ func (c *GenerateContext) newMiseStepBuilder() *MiseStepBuilder { SupportingAptPackages: []string{}, Assets: map[string]string{}, DependsOn: []string{}, - Outputs: []string{"/mise/shims", "/mise/installs", "/usr/local/bin/mise", "/etc/mise/config.toml", "/root/.local/state/mise"}, + Outputs: &[]string{"/mise/shims", "/mise/installs", "/usr/local/bin/mise", "/etc/mise/config.toml", "/root/.local/state/mise"}, app: c.App, env: c.Env, } @@ -73,33 +74,35 @@ func (b *MiseStepBuilder) Name() string { func (b *MiseStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) { step := plan.NewStep(b.DisplayName) + if len(b.MisePackages) == 0 { + return step, nil + } + step.DependsOn = b.DependsOn miseCache := options.Caches.AddCache("mise", "/mise/cache") // Install mise - if len(b.MisePackages) > 0 { + step.AddCommands([]plan.Command{ + plan.NewVariableCommand("MISE_DATA_DIR", "/mise"), + plan.NewVariableCommand("MISE_CONFIG_DIR", "/mise"), + plan.NewVariableCommand("MISE_INSTALL_PATH", "/usr/local/bin/mise"), + plan.NewVariableCommand("MISE_CACHE_DIR", "/mise/cache"), + plan.NewPathCommand("/mise/shims"), + options.NewAptInstallCommand([]string{"curl", "ca-certificates"}), + plan.NewExecCommand("sh -c 'curl -fsSL https://mise.run | sh'", + plan.ExecOptions{ + CustomName: "install mise", + Caches: []string{miseCache}, + }), + }) + + // Add user mise config files if they exist + supportingMiseConfigFiles := b.GetSupportingMiseConfigFiles(b.app.Source) + for _, file := range supportingMiseConfigFiles { step.AddCommands([]plan.Command{ - plan.NewVariableCommand("MISE_DATA_DIR", "/mise"), - plan.NewVariableCommand("MISE_CONFIG_DIR", "/mise"), - plan.NewVariableCommand("MISE_INSTALL_PATH", "/usr/local/bin/mise"), - plan.NewVariableCommand("MISE_CACHE_DIR", "/mise/cache"), - plan.NewPathCommand("/mise/shims"), - options.NewAptInstallCommand([]string{"curl", "ca-certificates"}), - plan.NewExecCommand("sh -c 'curl -fsSL https://mise.run | sh'", - plan.ExecOptions{ - CustomName: "install mise", - Caches: []string{miseCache}, - }), + plan.NewCopyCommand(file, "/app/"+file), }) - - // Add user mise config files if they exist - supportingMiseConfigFiles := b.GetSupportingMiseConfigFiles(b.app.Source) - for _, file := range supportingMiseConfigFiles { - step.AddCommands([]plan.Command{ - plan.NewCopyCommand(file, "/app/"+file), - }) - } } // Setup apt commands @@ -130,6 +133,7 @@ func (b *MiseStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) { for k := range packagesToInstall { pkgNames = append(pkgNames, k) } + sort.Strings(pkgNames) step.AddCommands([]plan.Command{ plan.NewFileCommand("/etc/mise/config.toml", "mise.toml", plan.FileOptions{ @@ -144,6 +148,7 @@ func (b *MiseStepBuilder) Build(options *BuildStepOptions) (*plan.Step, error) { step.Assets = b.Assets step.Outputs = b.Outputs + step.UseSecrets = &[]bool{false}[0] return step, nil } diff --git a/core/plan/plan.go b/core/plan/plan.go index d0a41d2..e5693d5 100644 --- a/core/plan/plan.go +++ b/core/plan/plan.go @@ -1,10 +1,10 @@ package plan type BuildPlan struct { - Variables map[string]string `json:"variables,omitempty"` - Steps []Step `json:"steps,omitempty"` - Start Start `json:"start,omitempty"` - Caches map[string]*Cache `json:"caches,omitempty"` + Steps []Step `json:"steps,omitempty"` + Start Start `json:"start,omitempty"` + Caches map[string]*Cache `json:"caches,omitempty"` + Secrets []string `json:"secrets,omitempty"` } type Start struct { @@ -16,16 +16,14 @@ type Start struct { // $PATHs to be prefixed to the container's base $PATH Paths []string `json:"paths,omitempty"` - - // Environment variables to be made available to the container - Env map[string]string `json:"env,omitempty"` } func NewBuildPlan() *BuildPlan { return &BuildPlan{ - Variables: map[string]string{}, - Steps: []Step{}, - Start: Start{}, + Steps: []Step{}, + Start: Start{}, + Caches: make(map[string]*Cache), + Secrets: []string{}, } } diff --git a/core/plan/plan_test.go b/core/plan/plan_test.go index 22a6b68..b68d58e 100644 --- a/core/plan/plan_test.go +++ b/core/plan/plan_test.go @@ -2,57 +2,82 @@ package plan import ( "encoding/json" - "reflect" "testing" -) - -func (p *BuildPlan) Equal(other *BuildPlan) bool { - if !reflect.DeepEqual(p.Variables, other.Variables) { - return false - } - if !reflect.DeepEqual(p.Steps, other.Steps) { - return false - } - - return true -} + "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/require" +) func TestSerialization(t *testing.T) { - plan := NewBuildPlan() - plan.Variables["MISE_DATA_DIR"] = "/mise" - - plan.AddStep(Step{ - Name: "install", - Commands: []Command{ - NewExecCommand("apt-get update"), - NewExecCommand("apt-get install -y curl"), + jsonPlan := `{ + "steps": [ + { + "name": "install", + "commands": [ + {"cmd": "apt-get update"}, + {"cmd": "apt-get install -y curl"} + ], + "startingImage": "ubuntu:22.04" + }, + { + "name": "deps", + "dependsOn": ["install"], + "commands": [ + {"path": "/root/.npm", "name": ".npmrc"}, + {"cmd": "npm ci"}, + {"cmd": "npm run build"} + ], + "useSecrets": true, + "outputs": [ + "dist", + "node_modules/.cache" + ], + "assets": { + "npmrc": "registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${NPM_TOKEN}\nalways-auth=true" + } + }, + { + "name": "build", + "dependsOn": ["deps"], + "commands": [ + {"src": ".", "dest": "."}, + {"cmd": "npm run test"}, + {"path": "/usr/local/bin"}, + {"name": "NODE_ENV", "value": "production"} + ], + "useSecrets": false + } + ], + "start": { + "baseImage": "node:18-slim", + "cmd": "npm start", + "paths": ["/usr/local/bin", "/app/node_modules/.bin"] }, - }) - - plan.AddStep(Step{ - Name: "build", - DependsOn: []string{"install"}, - Commands: []Command{ - NewCopyCommand(".", "."), - NewExecCommand("bun i --no-save"), - NewPathCommand("/mise/shims"), - NewVariableCommand("MISE_DATA_DIR", "/mise"), + "caches": { + "npm": { + "directory": "/root/.npm", + "type": "shared" + }, + "build-cache": { + "directory": "node_modules/.cache", + "type": "locked" + } }, - }) + "secrets": ["NPM_TOKEN", "GITHUB_TOKEN"] + }` - serialized, err := json.MarshalIndent(plan, "", " ") - if err != nil { - t.Fatal(err) - } + var plan1 BuildPlan + err := json.Unmarshal([]byte(jsonPlan), &plan1) + require.NoError(t, err) + + serialized, err := json.MarshalIndent(&plan1, "", " ") + require.NoError(t, err) - plan2 := BuildPlan{} + var plan2 BuildPlan err = json.Unmarshal(serialized, &plan2) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) - if !plan.Equal(&plan2) { - t.Fatal("plans are not equal") + if diff := cmp.Diff(plan1, plan2); diff != "" { + t.Errorf("plans mismatch (-want +got):\n%s", diff) } } diff --git a/core/plan/step.go b/core/plan/step.go index 9e003c3..2b00e53 100644 --- a/core/plan/step.go +++ b/core/plan/step.go @@ -14,10 +14,13 @@ type Step struct { DependsOn []string `json:"dependsOn,omitempty" jsonschema:"description=The steps that this step depends on. The step will only run after all the steps in DependsOn have run"` // The commands to run in this step - Commands []Command `json:"commands,omitempty" jsonschema:"description=The commands to run in this step"` + Commands *[]Command `json:"commands,omitempty" jsonschema:"description=The commands to run in this step"` + + // Whether the commands executed in this step should have access to secrets + UseSecrets *bool `json:"useSecrets,omitempty" jsonschema:"description=Whether the commands executed in this step should have access to secrets"` // Paths that this step outputs. Only these paths will be available to the next step - Outputs []string `json:"outputs,omitempty" jsonschema:"description=Paths that this step outputs. Only these paths will be available to the next step"` + Outputs *[]string `json:"outputs,omitempty" jsonschema:"description=Paths that this step outputs. Only these paths will be available to the next step"` // The assets available to this step. The key is the name of the asset that is referenced in a file command Assets map[string]string `json:"assets,omitempty" jsonschema:"description=The assets available to this step. The key is the name of the asset that is referenced in a file command"` @@ -40,32 +43,16 @@ func (s *Step) DependOn(name string) { } func (s *Step) AddCommands(commands []Command) { - s.Commands = append(s.Commands, commands...) -} - -func MergeSteps(steps ...*Step) *Step { - if len(steps) == 0 { - return nil - } - - result := &Step{ - Name: steps[0].Name, - DependsOn: make([]string, 0), - Commands: make([]Command, 0), - } - - for _, step := range steps { - result.DependsOn = append(result.DependsOn, step.DependsOn...) - result.Commands = append(result.Commands, step.Commands...) + if s.Commands == nil { + s.Commands = &[]Command{} } - - return result + *s.Commands = append(*s.Commands, commands...) } func (s *Step) UnmarshalJSON(data []byte) error { type Alias Step aux := &struct { - Commands []json.RawMessage `json:"commands"` + Commands *[]json.RawMessage `json:"commands"` *Alias }{ Alias: (*Alias)(s), @@ -75,13 +62,15 @@ func (s *Step) UnmarshalJSON(data []byte) error { return err } - s.Commands = make([]Command, len(aux.Commands)) - for i, rawCmd := range aux.Commands { - cmd, err := UnmarshalCommand(rawCmd) - if err != nil { - return err + if aux.Commands != nil { + s.Commands = &[]Command{} + for _, rawCmd := range *aux.Commands { + cmd, err := UnmarshalCommand(rawCmd) + if err != nil { + return err + } + *s.Commands = append(*s.Commands, cmd) } - s.Commands[i] = cmd } return nil diff --git a/core/prettyPrint.go b/core/prettyPrint.go index ccb778b..7e9b0ef 100644 --- a/core/prettyPrint.go +++ b/core/prettyPrint.go @@ -178,9 +178,13 @@ func getStepsToPrint(br *BuildResult) []*plan.Step { return execSteps } -func getCommandsToPrint(commands []plan.Command) []plan.ExecCommand { +func getCommandsToPrint(commands *[]plan.Command) []plan.ExecCommand { + if commands == nil { + return []plan.ExecCommand{} + } + execCommands := []plan.ExecCommand{} - for _, cmd := range commands { + for _, cmd := range *commands { if execCmd, ok := cmd.(plan.ExecCommand); ok { execCommands = append(execCommands, execCmd) } diff --git a/core/providers/node/node_test.go b/core/providers/node/node_test.go index 860e601..4953425 100644 --- a/core/providers/node/node_test.go +++ b/core/providers/node/node_test.go @@ -15,7 +15,7 @@ func TestDetect(t *testing.T) { }{ { name: "npm", - path: "../../../examples/node-npm-latest", + path: "../../../examples/node-npm", want: true, }, { @@ -54,7 +54,7 @@ func TestPackageManager(t *testing.T) { }{ { name: "npm project", - path: "../../../examples/node-npm-latest", + path: "../../../examples/node-npm", packageManager: PackageManagerNpm, }, { diff --git a/core/providers/php/php_test.go b/core/providers/php/php_test.go index f572182..405bedb 100644 --- a/core/providers/php/php_test.go +++ b/core/providers/php/php_test.go @@ -25,7 +25,7 @@ func TestDetect(t *testing.T) { }, { name: "no php", - path: "../../../examples/node-npm-latest", + path: "../../../examples/node-npm", want: false, }, } diff --git a/core/utils/merge.go b/core/utils/merge.go new file mode 100644 index 0000000..8cf9844 --- /dev/null +++ b/core/utils/merge.go @@ -0,0 +1,134 @@ +package utils + +import ( + "reflect" +) + +// MergeStructs merges multiple structs of the same type, with later values taking precedence. +// Only non-zero values from later structs will override earlier values. +func MergeStructs(dst interface{}, srcs ...interface{}) { + for _, src := range srcs { + if src == nil { + continue + } + mergeStruct(dst, src) + } +} + +// mergeStruct merges two structs of the same type, taking non-zero values from src +func mergeStruct(dst, src interface{}) { + dstValue := reflect.ValueOf(dst).Elem() + srcValue := reflect.ValueOf(src) + + if srcValue.Kind() == reflect.Ptr { + srcValue = srcValue.Elem() + } + + // If it's not a struct, we can't iterate over fields + if srcValue.Kind() != reflect.Struct { + return + } + + for i := 0; i < srcValue.NumField(); i++ { + srcField := srcValue.Field(i) + dstField := dstValue.Field(i) + + switch srcField.Kind() { + case reflect.Map: + mergeMap(dstField, srcField) + + case reflect.Slice: + // This is specific behaviour for how we want to merge slices + // Always override with the non-nil slice + if !srcField.IsNil() { + dstField.Set(srcField) + } + + case reflect.Ptr: + mergePtr(dstField, srcField) + + case reflect.Struct: + mergeStruct(dstField.Addr().Interface(), srcField.Interface()) + + default: + if !isZeroValue(srcField) { + dstField.Set(srcField) + } + } + } +} + +func mergeMap(dst, src reflect.Value) { + if src.IsNil() { + return + } + + if dst.IsNil() { + dst.Set(reflect.MakeMap(src.Type())) + } + + for _, key := range src.MapKeys() { + srcValue := src.MapIndex(key) + dstValue := dst.MapIndex(key) + + if srcValue.Kind() == reflect.Ptr && srcValue.Elem().Kind() == reflect.Struct { + if !dstValue.IsValid() { + dst.SetMapIndex(key, srcValue) + } else { + mergeStruct(dstValue.Interface(), srcValue.Interface()) + } + continue + } + + dst.SetMapIndex(key, srcValue) + } +} + +func mergePtr(dst, src reflect.Value) { + if src.IsNil() { + return + } + + switch src.Elem().Kind() { + case reflect.Struct: + if dst.IsNil() { + dst.Set(reflect.New(src.Elem().Type())) + } + mergeStruct(dst.Interface(), src.Interface()) + + case reflect.Slice: + if dst.IsNil() || !src.Elem().IsNil() { + dst.Set(src) + } + + default: + dst.Set(src) + } +} + +// isZeroValue checks if a reflect.Value is its zero value +func isZeroValue(v reflect.Value) bool { + switch v.Kind() { + case reflect.Array, reflect.Map, reflect.Slice, reflect.String: + return v.Len() == 0 + case reflect.Bool: + return !v.Bool() + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return v.Int() == 0 + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: + return v.Uint() == 0 + case reflect.Float32, reflect.Float64: + return v.Float() == 0 + case reflect.Interface, reflect.Ptr: + return v.IsNil() + case reflect.Struct: + // For structs, check if all fields are zero values + for i := 0; i < v.NumField(); i++ { + if !isZeroValue(v.Field(i)) { + return false + } + } + return true + } + return false +} diff --git a/core/utils/merge_test.go b/core/utils/merge_test.go new file mode 100644 index 0000000..fa71da6 --- /dev/null +++ b/core/utils/merge_test.go @@ -0,0 +1,329 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +type TestStruct struct { + // Basic types + String string + Int int + Bool bool + + // Special types + StringPtr *string + IntSlice []int + StringMap map[string]string + CommandSlice *[]string + + // Nested structs + Nested NestedStruct + Deep DeepNestedStruct +} + +type NestedStruct struct { + Value string + IntValue int +} + +type DeepNestedStruct struct { + Level1 struct { + Value string + Level2 NestedStruct + IntMap map[string]int + } +} + +func TestMergeBasicTypes(t *testing.T) { + str1 := "value1" + str2 := "value2" + + tests := []struct { + name string + dst TestStruct + src TestStruct + expected TestStruct + }{ + { + name: "zero values should not override", + dst: TestStruct{ + String: "keep", + Int: 42, + }, + src: TestStruct{ + String: "", // zero value + Int: 0, // zero value + Bool: false, // zero value + }, + expected: TestStruct{ + String: "keep", + Int: 42, + }, + }, + { + name: "non-zero values should override", + dst: TestStruct{ + String: "old", + Int: 1, + }, + src: TestStruct{ + String: "new", + Int: 2, + Bool: true, + }, + expected: TestStruct{ + String: "new", + Int: 2, + Bool: true, + }, + }, + { + name: "pointers should override when non-nil", + dst: TestStruct{ + StringPtr: &str1, + }, + src: TestStruct{ + StringPtr: &str2, + }, + expected: TestStruct{ + StringPtr: &str2, + }, + }, + { + name: "nil pointers should not override", + dst: TestStruct{ + StringPtr: &str1, + }, + src: TestStruct{}, // nil pointer + expected: TestStruct{ + StringPtr: &str1, + }, + }, + } + + runTests(t, tests) +} + +func TestMergeSlices(t *testing.T) { + tests := []struct { + name string + dst TestStruct + src TestStruct + expected TestStruct + }{ + { + name: "nil slice should not override", + dst: TestStruct{ + IntSlice: []int{1, 2}, + }, + src: TestStruct{}, // nil slice + expected: TestStruct{ + IntSlice: []int{1, 2}, + }, + }, + { + name: "empty slice should override", + dst: TestStruct{ + IntSlice: []int{1, 2}, + }, + src: TestStruct{ + IntSlice: []int{}, + }, + expected: TestStruct{ + IntSlice: []int{}, + }, + }, + { + name: "non-empty slice should override", + dst: TestStruct{ + IntSlice: []int{1, 2}, + }, + src: TestStruct{ + IntSlice: []int{3, 4}, + }, + expected: TestStruct{ + IntSlice: []int{3, 4}, + }, + }, + { + name: "pointer to slice - nil should not override", + dst: TestStruct{ + CommandSlice: &[]string{"cmd1"}, + }, + src: TestStruct{}, // nil pointer + expected: TestStruct{ + CommandSlice: &[]string{"cmd1"}, + }, + }, + { + name: "pointer to slice - empty should override", + dst: TestStruct{ + CommandSlice: &[]string{"cmd1"}, + }, + src: TestStruct{ + CommandSlice: &[]string{}, + }, + expected: TestStruct{ + CommandSlice: &[]string{}, + }, + }, + } + + runTests(t, tests) +} + +func TestMergeMaps(t *testing.T) { + tests := []struct { + name string + dst TestStruct + src TestStruct + expected TestStruct + }{ + { + name: "nil map should not override", + dst: TestStruct{ + StringMap: map[string]string{"key1": "value1"}, + }, + src: TestStruct{}, // nil map + expected: TestStruct{ + StringMap: map[string]string{"key1": "value1"}, + }, + }, + { + name: "empty map should merge", + dst: TestStruct{ + StringMap: map[string]string{"key1": "value1"}, + }, + src: TestStruct{ + StringMap: map[string]string{}, + }, + expected: TestStruct{ + StringMap: map[string]string{"key1": "value1"}, + }, + }, + { + name: "maps should merge values", + dst: TestStruct{ + StringMap: map[string]string{ + "keep": "old", + "override": "old", + }, + }, + src: TestStruct{ + StringMap: map[string]string{ + "override": "new", + "add": "new", + }, + }, + expected: TestStruct{ + StringMap: map[string]string{ + "keep": "old", + "override": "new", + "add": "new", + }, + }, + }, + } + + runTests(t, tests) +} + +func TestMergeNestedStructs(t *testing.T) { + tests := []struct { + name string + dst TestStruct + src TestStruct + expected TestStruct + }{ + { + name: "zero value nested struct should not override", + dst: TestStruct{ + Nested: NestedStruct{ + Value: "keep", + IntValue: 42, + }, + }, + src: TestStruct{ + Nested: NestedStruct{}, // zero value + }, + expected: TestStruct{ + Nested: NestedStruct{ + Value: "keep", + IntValue: 42, + }, + }, + }, + { + name: "nested struct should merge fields", + dst: TestStruct{ + Deep: DeepNestedStruct{ + Level1: struct { + Value string + Level2 NestedStruct + IntMap map[string]int + }{ + Value: "old", + Level2: NestedStruct{ + Value: "old", + IntValue: 1, + }, + IntMap: map[string]int{"a": 1}, + }, + }, + }, + src: TestStruct{ + Deep: DeepNestedStruct{ + Level1: struct { + Value string + Level2 NestedStruct + IntMap map[string]int + }{ + Value: "new", + Level2: NestedStruct{ + Value: "new", + }, + IntMap: map[string]int{"b": 2}, + }, + }, + }, + expected: TestStruct{ + Deep: DeepNestedStruct{ + Level1: struct { + Value string + Level2 NestedStruct + IntMap map[string]int + }{ + Value: "new", + Level2: NestedStruct{ + Value: "new", + IntValue: 1, + }, + IntMap: map[string]int{ + "a": 1, + "b": 2, + }, + }, + }, + }, + }, + } + + runTests(t, tests) +} + +func runTests(t *testing.T, tests []struct { + name string + dst TestStruct + src TestStruct + expected TestStruct +}) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.dst + MergeStructs(&result, &tt.src) + require.Equal(t, tt.expected, result) + }) + } +} diff --git a/examples/node-npm-latest/index.js b/examples/node-npm/index.js similarity index 100% rename from examples/node-npm-latest/index.js rename to examples/node-npm/index.js diff --git a/examples/node-npm-latest/package-lock.json b/examples/node-npm/package-lock.json similarity index 100% rename from examples/node-npm-latest/package-lock.json rename to examples/node-npm/package-lock.json diff --git a/examples/node-npm-latest/package.json b/examples/node-npm/package.json similarity index 91% rename from examples/node-npm-latest/package.json rename to examples/node-npm/package.json index 625e3d3..3dd8ed4 100644 --- a/examples/node-npm-latest/package.json +++ b/examples/node-npm/package.json @@ -1,5 +1,5 @@ { - "name": "node-npm-latest", + "name": "node-npm", "version": "1.0.0", "main": "index.js", "scripts": { diff --git a/examples/secrets/railpack.json b/examples/secrets/railpack.json new file mode 100644 index 0000000..1e69400 --- /dev/null +++ b/examples/secrets/railpack.json @@ -0,0 +1,33 @@ +{ + "$schema": "../../test/schema.json", + + "secrets": ["MY_SECRET", "MY_OTHER_SECRET", "HELLO_WORLD"], + + "steps": { + "usesSecrets": { + "commands": [ + { "name": "NOT_SECRET", "value": "not secret" }, + { "src": ".", "dest": "." }, + "./run.sh" + ], + "useSecrets": true + }, + + "defaultsToUsing": { + "commands": [ + { "name": "NOT_SECRET", "value": "not secret" }, + { "src": ".", "dest": "." }, + "./run.sh" + ] + }, + + "doesNotUseSecrets": { + "commands": [{ "src": ".", "dest": "." }, "./run.sh true"], + "useSecrets": false + } + }, + + "start": { + "cmd": "./run.sh" + } +} diff --git a/examples/secrets/run.sh b/examples/secrets/run.sh new file mode 100755 index 0000000..d6ea8ec --- /dev/null +++ b/examples/secrets/run.sh @@ -0,0 +1,60 @@ +#!/bin/bash + +# cache bust 1 + +# Check if argument is provided and if it equals "true" +INVERT_BEHAVIOR=${1:-false} + +# List of required secrets +REQUIRED_SECRETS=( + "MY_SECRET" + "MY_OTHER_SECRET" + "HELLO_WORLD" + "NOT_SECRET" +) + +missing_secrets=() +defined_secrets=() +error_value_secrets=() + +echo "Checking for required secrets..." + +for secret in "${REQUIRED_SECRETS[@]}"; do + if [ -z "${!secret}" ]; then + missing_secrets+=("$secret") + if [ "$INVERT_BEHAVIOR" = "true" ]; then + echo "✅ Secret not defined (as required): $secret" + else + echo "❌ Missing secret: $secret" + fi + else + defined_secrets+=("$secret") + # Check if the secret value contains "error" + if [ "${!secret}" = "error" ]; then + error_value_secrets+=("$secret") + echo "❌ Secret $secret contains invalid value 'error'" + elif [ "$INVERT_BEHAVIOR" = "true" ]; then + echo "❌ Found secret when it should be undefined: $secret" + else + echo "✅ Found secret: $secret = ${!secret}" + fi + fi +done + +if [ "$INVERT_BEHAVIOR" = "true" ]; then + if [ ${#defined_secrets[@]} -ne 0 ]; then + echo "Error: Found ${#defined_secrets[@]} secrets when none should be defined" + exit 1 + fi + echo "Success: No secrets are defined (as required)" +else + if [ ${#missing_secrets[@]} -ne 0 ]; then + echo "Error: Missing ${#missing_secrets[@]} required secrets" + exit 1 + fi + if [ ${#error_value_secrets[@]} -ne 0 ]; then + echo "Error: ${#error_value_secrets[@]} secrets contain the invalid value 'error'" + exit 1 + fi + echo "Success: All required secrets are available and valid" +fi diff --git a/go.mod b/go.mod index dedde19..219758c 100644 --- a/go.mod +++ b/go.mod @@ -7,54 +7,61 @@ require ( github.com/bmatcuk/doublestar/v4 v4.8.0 github.com/charmbracelet/lipgloss v1.0.0 github.com/charmbracelet/log v0.4.0 + github.com/gkampitakis/go-snaps v0.5.9 + github.com/google/go-cmp v0.6.0 github.com/invopop/jsonschema v0.13.0 - github.com/moby/buildkit v0.18.2 + github.com/moby/buildkit v0.19.0 github.com/opencontainers/image-spec v1.1.0 github.com/pkg/errors v0.9.1 github.com/stretchr/objx v0.5.2 - github.com/stretchr/testify v1.9.0 - github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184 + github.com/stretchr/testify v1.10.0 + github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a github.com/urfave/cli/v3 v3.0.0-beta1 gopkg.in/yaml.v2 v2.4.0 ) require ( - github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/charmbracelet/x/ansi v0.4.2 // indirect github.com/containerd/console v1.0.4 // indirect - github.com/containerd/containerd v1.7.24 // indirect - github.com/containerd/containerd/api v1.7.19 // indirect + github.com/containerd/containerd/api v1.8.0 // indirect + github.com/containerd/containerd/v2 v2.0.2 // indirect github.com/containerd/continuity v0.4.5 // indirect - github.com/containerd/errdefs v0.3.0 // indirect + github.com/containerd/errdefs v1.0.0 // indirect + github.com/containerd/errdefs/pkg v0.3.0 // indirect github.com/containerd/log v0.1.0 // indirect - github.com/containerd/platforms v0.2.1 // indirect - github.com/containerd/ttrpc v1.2.5 // indirect + github.com/containerd/platforms v1.0.0-rc.1 // indirect + github.com/containerd/ttrpc v1.2.7 // indirect github.com/containerd/typeurl/v2 v2.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v27.4.0-rc.2+incompatible // indirect + github.com/docker/cli v27.5.0+incompatible // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/gkampitakis/ciinfo v0.3.1 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect github.com/go-logfmt/logfmt v0.6.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect + github.com/goccy/go-yaml v1.15.13 // indirect github.com/gofrs/flock v0.12.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/in-toto/in-toto-golang v0.5.0 // indirect github.com/klauspost/compress v1.17.11 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/maruel/natural v1.1.1 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.15 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect @@ -67,34 +74,38 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/rogpeppe/go-internal v1.13.1 // indirect github.com/secure-systems-lab/go-securesystemslib v0.4.0 // indirect github.com/shibumi/go-pathspec v1.3.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 // indirect github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect - go.opentelemetry.io/otel v1.28.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect - go.opentelemetry.io/otel/metric v1.28.0 // indirect - go.opentelemetry.io/otel/sdk v1.28.0 // indirect - go.opentelemetry.io/otel/trace v1.28.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect + go.opentelemetry.io/otel v1.31.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 // indirect + go.opentelemetry.io/otel/metric v1.31.0 // indirect + go.opentelemetry.io/otel/sdk v1.31.0 // indirect + go.opentelemetry.io/otel/trace v1.31.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.6.0 // indirect - google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect - google.golang.org/grpc v1.66.3 // indirect - google.golang.org/protobuf v1.35.1 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 // indirect + google.golang.org/grpc v1.68.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.1 // indirect ) diff --git a/go.sum b/go.sum index 4fcfbc7..fc1cdc4 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,13 @@ -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0 h1:59MxjQVfjXsBpLy+dbd2/ELV5ofnUkUZBvWSC85sheA= -github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20230306123547-8075edf89bb0/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2 h1:dIScnXFlF784X79oi7MzVT6GWqr/W1uUt0pB5CsDs9M= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20231105174938-2b5cbb29f3e2/go.mod h1:gCLVsLfv1egrcZu+GoJATN5ts75F2s62ih/457eWzOw= github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= -github.com/Microsoft/hcsshim v0.12.8 h1:BtDWYlFMcWhorrvSSo2M7z0csPdw6t7no/C3FsSvqiI= -github.com/Microsoft/hcsshim v0.12.8/go.mod h1:cibQ4BqhJ32FXDwPdQhKhwrwophnh3FuT4nwQZF907w= +github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= +github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092/go.mod h1:rYqSE9HbjzpHTI74vwPvae4ZVYZd1lue2ta6xHPdblA= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= @@ -28,36 +24,37 @@ github.com/charmbracelet/log v0.4.0 h1:G9bQAcx8rWA2T3pWvx7YtPTPwgqpk7D68BX21IRW8 github.com/charmbracelet/log v0.4.0/go.mod h1:63bXt/djrizTec0l11H20t8FDSvA4CRZJ1KH22MdptM= github.com/charmbracelet/x/ansi v0.4.2 h1:0JM6Aj/g/KC154/gOP4vfxun0ff6itogDYk41kof+qk= github.com/charmbracelet/x/ansi v0.4.2/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b h1:ga8SEFjZ60pxLcmhnThWgvH2wg8376yUJmPhEH4H3kw= -github.com/cncf/xds/go v0.0.0-20240423153145-555b57ec207b/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb h1:EDmT6Q9Zs+SbUoc7Ik9EfrFqcylYqgPZ9ANSbTAntnE= github.com/codahale/rfc6979 v0.0.0-20141003034818-6a90f24967eb/go.mod h1:ZjrT6AXHbDs86ZSdt/osfBi5qfexBrKUdONk989Wnk4= -github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro= github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= -github.com/containerd/containerd v1.7.24 h1:zxszGrGjrra1yYJW/6rhm9cJ1ZQ8rkKBR48brqsa7nA= -github.com/containerd/containerd v1.7.24/go.mod h1:7QUzfURqZWCZV7RLNEn1XjUCQLEf0bkaK4GjUaZehxw= -github.com/containerd/containerd/api v1.7.19 h1:VWbJL+8Ap4Ju2mx9c9qS1uFSB1OVYr5JJrW2yT5vFoA= -github.com/containerd/containerd/api v1.7.19/go.mod h1:fwGavl3LNwAV5ilJ0sbrABL44AQxmNjDRcwheXDb6Ig= +github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0= +github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc= +github.com/containerd/containerd/v2 v2.0.2 h1:GmH/tRBlTvrXOLwSpWE2vNAm8+MqI6nmxKpKBNKY8Wc= +github.com/containerd/containerd/v2 v2.0.2/go.mod h1:wIqEvQ/6cyPFUGJ5yMFanspPabMLor+bF865OHvNTTI= github.com/containerd/continuity v0.4.5 h1:ZRoN1sXq9u7V6QoHMcVWGhOwDFqZ4B9i5H6un1Wh0x4= github.com/containerd/continuity v0.4.5/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= -github.com/containerd/errdefs v0.3.0 h1:FSZgGOeK4yuT/+DnF07/Olde/q4KBoMsaamhXxIMDp4= -github.com/containerd/errdefs v0.3.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= +github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= +github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= +github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= -github.com/containerd/nydus-snapshotter v0.14.0 h1:6/eAi6d7MjaeLLuMO8Udfe5GVsDudmrDNO4SGETMBco= -github.com/containerd/nydus-snapshotter v0.14.0/go.mod h1:TT4jv2SnIDxEBu4H2YOvWQHPOap031ydTaHTuvc5VQk= -github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= -github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= -github.com/containerd/stargz-snapshotter v0.15.1 h1:fpsP4kf/Z4n2EYnU0WT8ZCE3eiKDwikDhL6VwxIlgeA= -github.com/containerd/stargz-snapshotter/estargz v0.15.1 h1:eXJjw9RbkLFgioVaTG+G/ZW/0kEe2oEKCdS/ZxIyoCU= -github.com/containerd/stargz-snapshotter/estargz v0.15.1/go.mod h1:gr2RNwukQ/S9Nv33Lt6UC7xEx58C+LHRdoqbEKjz1Kk= -github.com/containerd/ttrpc v1.2.5 h1:IFckT1EFQoFBMG4c3sMdT8EP3/aKfumK1msY+Ze4oLU= -github.com/containerd/ttrpc v1.2.5/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= +github.com/containerd/nydus-snapshotter v0.15.0 h1:RqZRs1GPeM6T3wmuxJV9u+2Rg4YETVMwTmiDeX+iWC8= +github.com/containerd/nydus-snapshotter v0.15.0/go.mod h1:biq0ijpeZe0I5yZFSJyHzFSjjRZQ7P7y/OuHyd7hYOw= +github.com/containerd/platforms v1.0.0-rc.1 h1:83KIq4yy1erSRgOVHNk1HYdPvzdJ5CnsWaRoJX4C41E= +github.com/containerd/platforms v1.0.0-rc.1/go.mod h1:J71L7B+aiM5SdIEqmd9wp6THLVRzJGXfNuWCZCllLA4= +github.com/containerd/plugin v1.0.0 h1:c8Kf1TNl6+e2TtMHZt+39yAPDbouRH9WAToRjex483Y= +github.com/containerd/plugin v1.0.0/go.mod h1:hQfJe5nmWfImiqT1q8Si3jLv3ynMUIBB47bQ+KexvO8= +github.com/containerd/stargz-snapshotter v0.16.3 h1:zbQMm8dRuPHEOD4OqAYGajJJUwCeUzt4j7w9Iaw58u4= +github.com/containerd/stargz-snapshotter/estargz v0.16.3 h1:7evrXtoh1mSbGj/pfRccTampEyKpjpOnS3CyiV1Ebr8= +github.com/containerd/stargz-snapshotter/estargz v0.16.3/go.mod h1:uyr4BfYfOj3G9WBVE8cOlQmXAbPN9VEQpBBeJIuOipU= +github.com/containerd/ttrpc v1.2.7 h1:qIrroQvuOL9HQ1X6KHe2ohc7p+HP/0VE6XPU7elJRqQ= +github.com/containerd/ttrpc v1.2.7/go.mod h1:YCXHsb32f+Sq5/72xHubdiJRQY9inL4a4ZQrAbN1q9o= github.com/containerd/typeurl/v2 v2.2.3 h1:yNA/94zxWdvYACdYO8zofhrTVuQY73fFU1y++dYSw40= github.com/containerd/typeurl/v2 v2.2.3/go.mod h1:95ljDnPfD3bAbDJRugOiShd/DlAAsxGtUBhJxIn7SCk= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -66,20 +63,24 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v27.4.0-rc.2+incompatible h1:A0GZwegDlt2wdt3tpmrUzkVOZmbhvd7i05wPSf7Oo74= -github.com/docker/cli v27.4.0-rc.2+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= -github.com/docker/docker v27.4.0-rc.2+incompatible h1:9OJjVGtelk/zGC3TyKweJ29b9Axzh0s/0vtU4mneumE= -github.com/docker/docker v27.4.0-rc.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/cli v27.5.0+incompatible h1:aMphQkcGtpHixwwhAXJT1rrK/detk2JIvDaFkLctbGM= +github.com/docker/cli v27.5.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= +github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= -github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= -github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/gkampitakis/ciinfo v0.3.1 h1:lzjbemlGI4Q+XimPg64ss89x8Mf3xihJqy/0Mgagapo= +github.com/gkampitakis/ciinfo v0.3.1/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.5.9 h1:jO2Fe2q2fhLNcMQjYh/EAGUzrZiIdwD/CkM8+QSs5cE= +github.com/gkampitakis/go-snaps v0.5.9/go.mod h1:PcKmy8q5Se7p48ywpogN5Td13reipz1Iivah4wrTIvY= github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4= github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -87,6 +88,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/goccy/go-yaml v1.15.13 h1:Xd87Yddmr2rC1SLLTm2MNDcTjeO/GYo0JGiww6gSTDg= +github.com/goccy/go-yaml v1.15.13/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -101,8 +104,8 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaU github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -125,12 +128,14 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= +github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/moby/buildkit v0.18.2 h1:l86uBvxh4ntNoUUg3Y0eGTbKg1PbUh6tawJ4Xt75SpQ= -github.com/moby/buildkit v0.18.2/go.mod h1:vCR5CX8NGsPTthTg681+9kdmfvkvqJBXEv71GZe5msU= +github.com/moby/buildkit v0.19.0 h1:w9G1p7sArvCGNkpWstAqJfRQTXBKukMyMK1bsah1HNo= +github.com/moby/buildkit v0.19.0/go.mod h1:WiHBFTgWV8eB1AmPxIWsAlKjUACAwm3X/14xOV4VWew= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= @@ -157,10 +162,13 @@ github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQ github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= github.com/opencontainers/runtime-spec v1.2.0 h1:z97+pHb3uELt/yiAWD691HNHQIF07bE7dzrbT927iTk= github.com/opencontainers/runtime-spec v1.2.0/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= -github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= +github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626 h1:DmNGcqH3WDbV5k8OJ+esPWbqUOX5rMLR2PMvziDMJi0= +github.com/opencontainers/runtime-tools v0.9.1-0.20221107090550-2e043c6bd626/go.mod h1:BRHJJd0E+cx42OybVYSgUvZmU0B8P9gZuRXlZUP7TKI= +github.com/opencontainers/selinux v1.11.1 h1:nHFvthhM0qY8/m+vfhJylliSshm8G1jJ2jDMcgULaH8= +github.com/opencontainers/selinux v1.11.1/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= @@ -172,8 +180,9 @@ github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoG github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= -github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= github.com/secure-systems-lab/go-securesystemslib v0.4.0/go.mod h1:FGBZgq2tXWICsxWQW1msNf49F0Pf2Op5Htayx335Qbs= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= @@ -187,10 +196,22 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184 h1:RgyoSI38Y36zjQaszel/0RAcIehAnjA1B0RiUV9SDO4= -github.com/tonistiigi/fsutil v0.0.0-20241121093142-31cf1f437184/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 h1:kdXcSzyDtseVEc4yCz2qF8ZrQvIDBJLl4S1c3GCXmoI= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a h1:EfGw4G0x/8qXWgtcZ6KVaPS+wpWOQMaypczzP8ojkMY= +github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw= github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8= github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE= github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0= @@ -199,37 +220,37 @@ github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Q github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab/go.mod h1:ulncasL3N9uLrVann0m+CDlJKWsIAP34MPcOJF6VRvc= github.com/urfave/cli/v3 v3.0.0-beta1 h1:6DTaaUarcM0wX7qj5Hcvs+5Dm3dyUTBbEwIWAjcw9Zg= github.com/urfave/cli/v3 v3.0.0-beta1/go.mod h1:FnIeEMYu+ko8zP1F9Ypr3xkZMIDqW3DR92yUtY39q1Y= -github.com/vbatts/tar-split v0.11.5 h1:3bHCTIheBm1qFTcgh9oPu+nNBtX+XJIupG/vacinCts= -github.com/vbatts/tar-split v0.11.5/go.mod h1:yZbwRsSeGjusneWgA781EKej9HF8vme8okylkAeNKLk= +github.com/vbatts/tar-split v0.11.6 h1:4SjTW5+PU11n6fZenf2IPoV8/tz3AaYHMWjf23envGs= +github.com/vbatts/tar-split v0.11.6/go.mod h1:dqKNtesIOr2j2Qv3W/cHjnvk9I8+G7oAkFDFN6TCBEI= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1 h1:gbhw/u49SS3gkPWiYweQNJGm/uJN5GkI/FrosxSHT7A= -go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.46.1/go.mod h1:GnOaBaFQ2we3b9AGWJpsBa7v1S5RlQzlC3O7dRMxZhM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 h1:4K4tsIXefpVJtvA/8srF4V4y0akAoPHkIslgAkjixJA= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0/go.mod h1:jjdQuTGVsXV4vSs+CJ2qYDeDPf9yIJV23qlIzBm73Vg= -go.opentelemetry.io/otel v1.28.0 h1:/SqNcYk+idO0CxKEUOtKQClMK/MimZihKYMruSMViUo= -go.opentelemetry.io/otel v1.28.0/go.mod h1:q68ijF8Fc8CnMHKyzqL6akLO46ePnjkgfIMIjUIX9z4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 h1:3Q/xZUyC1BBkualc9ROb4G8qkH90LXEIICcs5zv1OYY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0/go.mod h1:s75jGIWA9OfCMzF0xr+ZgfrB5FEbbV7UuYo32ahUiFI= -go.opentelemetry.io/otel/metric v1.28.0 h1:f0HGvSl1KRAU1DLgLGFjrwVyismPlnuU6JD6bOeuA5Q= -go.opentelemetry.io/otel/metric v1.28.0/go.mod h1:Fb1eVBFZmLVTMb6PPohq3TO9IIhUisDsbJoL/+uQW4s= -go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= -go.opentelemetry.io/otel/sdk v1.28.0/go.mod h1:oYj7ClPUA7Iw3m+r7GeEjz0qckQRJK2B8zjcZEfu7Pg= -go.opentelemetry.io/otel/trace v1.28.0 h1:GhQ9cUuQGmNDd5BTCP2dAvv75RdMxEfTmYejp+lkx9g= -go.opentelemetry.io/otel/trace v1.28.0/go.mod h1:jPyXzNPg6da9+38HEwElrQiHlVMTnVfM3/yv2OlIHaI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0 h1:yMkBS9yViCc7U7yeLzJPM2XizlfdVvBRSmsQDWu6qc0= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.56.0/go.mod h1:n8MR6/liuGB5EmTETUBeU5ZgqMOlqKRxUaqPQBOANZ8= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0 h1:4BZHA+B1wXEQoGNHxW8mURaLhcdGwvRnmhGbm+odRbc= +go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.56.0/go.mod h1:3qi2EEwMgB4xnKgPLqsDP3j9qxnHDZeHsnAxfjQqTko= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.31.0 h1:NsJcKPIW0D0H3NgzPDHmo0WW6SptzPdqg/L1zsIm2hY= +go.opentelemetry.io/otel v1.31.0/go.mod h1:O0C14Yl9FgkjqcCZAsE053C13OaddMYr/hz6clDkEJE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0 h1:K0XaT3DwHAcV4nKLzcQvwAgSyisUghWoY20I7huthMk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.31.0/go.mod h1:B5Ki776z/MBnVha1Nzwp5arlzBbE3+1jk+pGmaP5HME= +go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE= +go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY= +go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk= +go.opentelemetry.io/otel/sdk v1.31.0/go.mod h1:TfRbMdhvxIIr/B2N2LQW2S5v9m3gOQ/08KsbbO5BPT0= +go.opentelemetry.io/otel/trace v1.31.0 h1:ffjsj1aRouKewfr85U2aGagJ46+MvodynlQ1HYdmJys= +go.opentelemetry.io/otel/trace v1.31.0/go.mod h1:TXZkRk7SM2ZQLtR6eoAWQFIHPvzQ06FJAsO1tJg480A= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -240,29 +261,27 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -273,16 +292,14 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= -google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094 h1:0+ozOGcrp+Y8Aq8TLNN2Aliibms5LEzsq99ZZmAGYm0= -google.golang.org/genproto/googleapis/api v0.0.0-20240701130421-f6361c86f094/go.mod h1:fJ/e3If/Q67Mj99hin0hMhiNyCRmt6BQ2aWIJshUSJw= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.66.3 h1:TWlsh8Mv0QI/1sIbs1W36lqRclxrmF+eFJ4DbI0fuhA= -google.golang.org/grpc v1.66.3/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA= -google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9 h1:T6rh4haD3GVYsgEfWExoCZA2o2FmbNyKpTuAxbEFPTg= +google.golang.org/genproto/googleapis/api v0.0.0-20241007155032-5fefd90f89a9/go.mod h1:wp2WsuBYj6j8wUdo3ToZsdxxixbvQNAHqVJrTgi5E5M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38 h1:zciRKQ4kBpFgpfC5QQCVtnnNAcLIqweL7plyZRQHVpI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241021214115-324edc3d5d38/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -293,3 +310,9 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU= gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= +sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +tags.cncf.io/container-device-interface v0.8.0 h1:8bCFo/g9WODjWx3m6EYl3GfUG31eKJbaggyBDxEldRc= +tags.cncf.io/container-device-interface v0.8.0/go.mod h1:Apb7N4VdILW0EVdEMRYXIDVRZfNJZ+kmEUss2kRRQ6Y= +tags.cncf.io/container-device-interface/specs-go v0.8.0 h1:QYGFzGxvYK/ZLMrjhvY0RjpUavIn4KcmRmVP/JjdBTA= +tags.cncf.io/container-device-interface/specs-go v0.8.0/go.mod h1:BhJIkjjPh4qpys+qm4DAYtUyryaTDg9zris+AczXyws= diff --git a/mise.toml b/mise.toml index 0a18a8a..c0e8549 100644 --- a/mise.toml +++ b/mise.toml @@ -7,6 +7,9 @@ run = "rm -rf bin" [tasks.test] run = "go test ./..." +[tasks.test-update-snapshots] +run = "UPDATE_SNAPS=true go test ./..." + [tasks.check] run = """ go vet ./... @@ -24,6 +27,9 @@ echo 'FROM alpine:latest RUN echo "Hello"' | docker buildx build -f - . """ +[tasks.run-with-frontend] +run = "bun scripts/run-with-frontend.ts" + [tasks.build-and-push-frontend] run = """ docker build -t railpack-frontend:latest \ diff --git a/package.json b/package.json new file mode 100644 index 0000000..a1b33d6 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "railpack-go", + "module": "index.ts", + "type": "module", + "devDependencies": { + "@types/bun": "latest", + "@types/node": "^22.12.0" + }, + "peerDependencies": { + "typescript": "^5.0.0" + } +} \ No newline at end of file diff --git a/scripts/config/template.mjs b/scripts/config/template.mjs deleted file mode 100644 index 1c12299..0000000 --- a/scripts/config/template.mjs +++ /dev/null @@ -1,24 +0,0 @@ -import { readFile, writeFile } from "fs/promises"; -import { getNixPath } from "../util/nix.mjs"; - -const replaceStr = input => - input - // If statements - .replaceAll(/\$if\s*\((\w+)\)\s*\(([^]*?)\)\s*else\s*\(([^]*?)\)/gm, - (_all, condition, value, otherwise) => - process.env[condition] ? replaceStr(value) : replaceStr(otherwise) - ) - // Variables - .replaceAll(/\${(\w+)}/g, - (_all, name) => process.env[name] - ) - // Nix paths - .replaceAll(/\$!{(\w+)}/g, - (_all, exe) => getNixPath(exe) - ) - -export async function compileTemplate(infile, outfile) { - await writeFile(outfile, - replaceStr(await readFile(infile, { encoding: 'utf8' })), - { encoding: 'utf8' }) -} diff --git a/scripts/prestart.mjs b/scripts/prestart.mjs deleted file mode 100755 index a957f9f..0000000 --- a/scripts/prestart.mjs +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env node -import { compileTemplate } from "./config/template.mjs"; -import { e } from "./util/cmd.mjs"; -import { checkEnvErrors, isLaravel } from "./util/laravel.mjs"; -import Logger from "./util/logger.mjs"; -import { access, constants } from 'node:fs/promises' - -const prestartLogger = new Logger('prestart'); -const serverLogger = new Logger('server'); - -if (process.argv.length != 4) { - prestartLogger.error(`Usage: ${process.argv[1]} `) - process.exit(1); -} - -await Promise.all([ - isLaravel() ? checkEnvErrors('/app') : Promise.resolve(), - access('/app/storage', constants.R_OK) - .then(() => e('chmod -R ugo+rw /app/storage')) - .catch(() => {}), - compileTemplate(process.argv[2], process.argv[3]) -]).catch(err => prestartLogger.error(err)); - -serverLogger.info(`Server starting on port ${process.env.PORT}`) diff --git a/scripts/run-with-frontend.ts b/scripts/run-with-frontend.ts new file mode 100644 index 0000000..b097a13 --- /dev/null +++ b/scripts/run-with-frontend.ts @@ -0,0 +1,119 @@ +import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import crypto from "node:crypto"; + +const PLAN_FILE = "railpack-plan.json"; +const FRONTEND_IMAGE = "ghcr.io/railwayapp/railpack:railpack-frontend"; + +// Parse command line arguments +const args = process.argv.slice(2); +if (args.length === 0) { + console.error("Please provide a directory path"); + process.exit(1); +} + +const dir = args[0]; +const envArgs = args.slice(1).filter((arg) => arg.startsWith("--env")); + +// Create temp directory to save the build plan to +const randId = Math.random().toString(36).slice(2); +const tmpDir = join("/tmp", "railpack-" + randId); +const planDir = join(tmpDir, "plan"); +mkdirSync(planDir, { recursive: true }); + +const cleanup = () => { + if (existsSync(tmpDir)) { + rmSync(tmpDir, { recursive: true, force: true }); + } +}; + +process.on("exit", cleanup); +process.on("SIGINT", () => { + cleanup(); + process.exit(); +}); + +// Generate build plan +console.log(`Generating build plan for ${dir}`); +const planResult = spawnSync( + "go", + ["run", "cmd/cli/main.go", "plan", dir, "--format", "json"], + { + stdio: ["inherit", "pipe", "inherit"], + } +); + +if (planResult.status !== 0) { + console.error("Failed to generate build plan"); + process.exit(1); +} + +const planPath = join(planDir, PLAN_FILE); +writeFileSync(planPath, planResult.stdout); + +// Parse all env vars so that we can use them as secrets +const envVars: Record = {}; +const secretArgs: string[] = []; + +// Find all env args and their values +for (let i = 0; i < args.length; i++) { + if (args[i] === "--env" && i + 1 < args.length) { + const nameValue = args[i + 1]; + const [name, value] = nameValue.split("="); + if (name && value) { + envVars[name] = value; + secretArgs.push(`--secret=id=${name},env=${name}`); + } + i++; // Skip the next argument since we've processed it + } +} + +// Pipe buildctl and docker load together +const buildctlArgs = [ + "build", + `--local`, + `context=${dir}`, + `--local`, + `dockerfile=${planDir}`, + "--frontend=gateway.v0", + "--opt", + `source=${FRONTEND_IMAGE}`, + "--output", + "type=docker,name=test", + ...secretArgs, +]; + +// Options that are passed to our custom frontend +const cacheKey = dir; +buildctlArgs.push("--opt", `cache-key=${cacheKey}`); + +if (Object.keys(envVars).length > 0) { + const secretsHash = crypto + .createHash("sha256") + .update(Object.values(envVars).sort().join("")) + .digest("hex"); + buildctlArgs.push("--opt", `secrets-hash=${secretsHash}`); +} + +console.log(`Executing buildctl\n ${buildctlArgs.join(" ")}`); + +const buildctl = spawnSync("buildctl", buildctlArgs, { + stdio: ["inherit", "pipe", "inherit"], + env: { ...process.env, ...envVars }, +}); + +if (buildctl.status !== 0) { + console.error("buildctl command failed"); + process.exit(1); +} + +const dockerLoad = spawnSync("docker", ["load"], { + input: buildctl.stdout, + stdio: ["pipe", "inherit", "inherit"], +}); + +if (dockerLoad.status !== 0) { + console.error("docker load failed"); + process.exit(1); +} diff --git a/scripts/util/cmd.mjs b/scripts/util/cmd.mjs deleted file mode 100644 index e079dca..0000000 --- a/scripts/util/cmd.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { execSync } from "child_process"; - -export const e = cmd => execSync(cmd).toString().replace('\n', ''); \ No newline at end of file diff --git a/scripts/util/laravel.mjs b/scripts/util/laravel.mjs deleted file mode 100644 index e5806fc..0000000 --- a/scripts/util/laravel.mjs +++ /dev/null @@ -1,41 +0,0 @@ -import Logger from "./logger.mjs" -import * as fs from 'node:fs/promises' -import * as path from 'node:path' - -const variableHints = { - 'APP_ENV': 'You should probably set this to `production`.' -}; - -const logger = new Logger('laravel'); - -export const isLaravel = () => process.env['IS_LARAVEL'] != null; - -function checkVariable(name) { - if (!process.env[name]) { - let hint = - `Your app configuration references the ${name} environment variable, but it is not set.` - + (variableHints[name] ?? ''); - - logger.warn(hint); - } -} - -export async function checkEnvErrors(srcdir) { - const envRegex = /env\(["']([^,]*)["']\)/g; - const configDir = path.join(srcdir, 'config'); - - const config = - (await Promise.all( - (await fs.readdir(configDir)) - .filter(fileName => fileName.endsWith('.php')) - .map(fileName => fs.readFile(path.join(configDir, fileName))) - )).join(''); - - for (const match of config.matchAll(envRegex)) { - if (match[1] != 'APP_KEY') checkVariable(match[1]); - } - - if (!process.env.APP_KEY) { - logger.warn('Your app key is not set! Please set a random 32-character string in your APP_KEY environment variable. This can be easily generated with `openssl rand -hex 16`.'); - } -} diff --git a/scripts/util/logger.mjs b/scripts/util/logger.mjs deleted file mode 100644 index 83db677..0000000 --- a/scripts/util/logger.mjs +++ /dev/null @@ -1,27 +0,0 @@ -export default class Logger { - /** @type string */ - #tag; - - /** - * @param {string} tag - */ - constructor(tag) { - this.#tag = tag - } - - #log(color, messageType, message, fn = console.log) { - fn(`\x1b[${color}m[${this.#tag}:${messageType}]\x1b[0m ${message}`) - } - - info(message) { - this.#log(34, 'info', message) - } - - warn(message) { - this.#log(35, 'warn', message, console.warn) - } - - error(message) { - this.#log(31, 'error', message, console.error) - } -} diff --git a/scripts/util/nix.mjs b/scripts/util/nix.mjs deleted file mode 100644 index d86ef1e..0000000 --- a/scripts/util/nix.mjs +++ /dev/null @@ -1,3 +0,0 @@ -import { e } from "./cmd.mjs"; - -export const getNixPath = (exe) => e(`nix-store -q ${e(`which ${exe}`)}`); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..238655f --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + // Enable latest features + "lib": ["ESNext", "DOM"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}