diff --git a/docs/_includes/reference/vault_plugin/configure.md b/docs/_includes/reference/vault_plugin/configure.md index 90451fea..1eedf93b 100644 --- a/docs/_includes/reference/vault_plugin/configure.md +++ b/docs/_includes/reference/vault_plugin/configure.md @@ -9,6 +9,8 @@ Configure the plugin. ### Parameters +* `buildx_driver` (string, optional) — The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable. +* `buildx_driver_opts` (array, optional) — The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables. * `git_repo_url` (string, required) — URL of the Git repository. * `git_trdl_channels_branch` (string, optional) — A special Git branch to store the trdl channels configuration file. * `git_trdl_channels_path` (string, optional) — A path in the Git repository to the trdl channels configuration file (trdl_channels.yaml is used by default). diff --git a/docs/pages_en/QUICKSTART.md b/docs/pages_en/QUICKSTART.md index 4e289c07..5e085c5d 100644 --- a/docs/pages_en/QUICKSTART.md +++ b/docs/pages_en/QUICKSTART.md @@ -48,6 +48,20 @@ TRDL_BUILDX_DRIVER_OPTS_SEPARATOR=; TRDL_BUILDX_DRIVER_OPTS_KUBE='namespace=trdl-build;rootless=true' ``` +The same two settings are also available per project in the plugin configuration, which is the only way to reach them when the plugin is compiled into a host process whose environment the administrator cannot set: + +* `buildx_driver` — the same values as `TRDL_BUILDX_DRIVER`; +* `buildx_driver_opts` — a list of `--driver-opt` values, one per element, each passed through as is. + +```json +{ + "buildx_driver": "kubernetes", + "buildx_driver_opts": ["namespace=trdl-build", "rootless=true"] +} +``` + +Each of the two settings is resolved on its own: the plugin configuration takes precedence over the environment, and the environment takes precedence over the default `docker-container` driver with no options. A field left out of `configure`, or set to an empty value, means "not configured" and falls back to the environment — it does not override it with an empty value. To build with no driver options at all while the environment defines some, unset those variables. + Notes on the `kubernetes` driver: * the target namespace must exist, and the Vault process needs permissions to manage Deployments and Pods in it: the builder runs as a BuildKit Deployment and is removed after the build; diff --git a/docs/pages_ru/QUICKSTART.md b/docs/pages_ru/QUICKSTART.md index 4ade258b..6d15ed35 100644 --- a/docs/pages_ru/QUICKSTART.md +++ b/docs/pages_ru/QUICKSTART.md @@ -47,6 +47,20 @@ TRDL_BUILDX_DRIVER_OPTS_SEPARATOR=; TRDL_BUILDX_DRIVER_OPTS_KUBE='namespace=trdl-build;rootless=true' ``` +Те же две настройки доступны и в конфигурации плагина, отдельно для каждого проекта. Это единственный способ задать их, когда плагин вкомпилен в хост-процесс, окружением которого администратор не управляет: + +* `buildx_driver` — те же значения, что и у `TRDL_BUILDX_DRIVER`; +* `buildx_driver_opts` — список значений `--driver-opt`, по одному в элементе, каждое передаётся без изменений. + +```json +{ + "buildx_driver": "kubernetes", + "buildx_driver_opts": ["namespace=trdl-build", "rootless=true"] +} +``` + +Каждая из двух настроек разрешается независимо: конфигурация плагина имеет приоритет над переменными окружения, а переменные окружения — над умолчанием, то есть драйвером `docker-container` без опций. Поле, не переданное в `configure` или переданное пустым, означает «не задано» и отдаёт решение переменным окружения, а не перекрывает их пустым значением. Чтобы собирать вообще без опций драйвера, когда в окружении они заданы, эти переменные нужно снять. + Особенности драйвера `kubernetes`: * целевой namespace должен существовать, а процессу Vault нужны права на управление Deployment и Pod в нём: сборщик работает как BuildKit Deployment и удаляется после сборки; diff --git a/server/path_configure.go b/server/path_configure.go index 6ba28a09..f99f815c 100644 --- a/server/path_configure.go +++ b/server/path_configure.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/logical" + "github.com/werf/trdl/server/pkg/docker" "github.com/werf/trdl/server/pkg/elf_signing" "github.com/werf/trdl/server/pkg/git" "github.com/werf/trdl/server/pkg/mac_signing" @@ -30,6 +31,8 @@ const ( fieldNameS3AccessKeyID = "s3_access_key_id" fieldNameS3SecretAccessKey = "s3_secret_access_key" fieldNameS3BucketName = "s3_bucket_name" + fieldNameBuildxDriver = "buildx_driver" + fieldNameBuildxDriverOpts = "buildx_driver_opts" storageKeyConfiguration = "configuration" ) @@ -110,6 +113,16 @@ func configurePath(b *Backend) *framework.Path { Description: "The S3 storage secret access key", Required: true, }, + fieldNameBuildxDriver: { + Type: framework.TypeString, + Description: "The buildx driver to build release artifacts with: docker-container (used by default) or kubernetes. Takes precedence over the TRDL_BUILDX_DRIVER environment variable", + Required: false, + }, + fieldNameBuildxDriverOpts: { + Type: framework.TypeStringSlice, + Description: "The buildx driver options, one --driver-opt per element (e.g. namespace=trdl-build), passed through as is. Take precedence over the TRDL_BUILDX_DRIVER_OPTS_* environment variables", + Required: false, + }, }, Operations: map[logical.Operation]framework.OperationHandler{ logical.CreateOperation: &framework.PathOperation{ @@ -137,6 +150,10 @@ func (b *Backend) pathConfigureCreateOrUpdate(ctx context.Context, req *logical. return errResp, nil } + if err := docker.ValidateBuildxDriver(fields.Get(fieldNameBuildxDriver).(string)); err != nil { + return logical.ErrorResponse("%s validation failed: %s", fieldNameBuildxDriver, err), nil + } + cfg := &configuration{ GitRepoUrl: fields.Get(fieldNameGitRepoUrl).(string), GitTrdlPath: fields.Get(fieldNameGitTrdlPath).(string), @@ -149,6 +166,8 @@ func (b *Backend) pathConfigureCreateOrUpdate(ctx context.Context, req *logical. S3AccessKeyID: fields.Get(fieldNameS3AccessKeyID).(string), S3SecretAccessKey: fields.Get(fieldNameS3SecretAccessKey).(string), S3BucketName: fields.Get(fieldNameS3BucketName).(string), + BuildxDriver: fields.Get(fieldNameBuildxDriver).(string), + BuildxDriverOpts: fields.Get(fieldNameBuildxDriverOpts).([]string), } if err := putConfiguration(ctx, req.Storage, cfg); err != nil { @@ -180,17 +199,19 @@ func (b *Backend) pathConfigureDelete(ctx context.Context, req *logical.Request, } type configuration struct { - GitRepoUrl string `structs:"git_repo_url" json:"git_repo_url"` - GitTrdlPath string `structs:"git_trdl_path" json:"git_trdl_path"` - GitTrdlChannelsPath string `structs:"git_trdl_channels_path" json:"git_trdl_channels_path"` - GitTrdlChannelsBranch string `structs:"git_trdl_channels_branch" json:"git_trdl_channels_branch"` - InitialLastPublishedGitCommit string `structs:"initial_last_published_git_commit" json:"initial_last_published_git_commit"` - RequiredNumberOfVerifiedSignaturesOnCommit int `structs:"required_number_of_verified_signatures_on_commit" json:"required_number_of_verified_signatures_on_commit"` - S3Endpoint string `structs:"s3_endpoint" json:"s3_endpoint"` - S3Region string `structs:"s3_region" json:"s3_region"` - S3AccessKeyID string `structs:"s3_access_key_id" json:"s3_access_key_id"` - S3SecretAccessKey string `structs:"s3_secret_access_key" json:"s3_secret_access_key"` - S3BucketName string `structs:"s3_bucket_name" json:"s3_bucket_name"` + GitRepoUrl string `structs:"git_repo_url" json:"git_repo_url"` + GitTrdlPath string `structs:"git_trdl_path" json:"git_trdl_path"` + GitTrdlChannelsPath string `structs:"git_trdl_channels_path" json:"git_trdl_channels_path"` + GitTrdlChannelsBranch string `structs:"git_trdl_channels_branch" json:"git_trdl_channels_branch"` + InitialLastPublishedGitCommit string `structs:"initial_last_published_git_commit" json:"initial_last_published_git_commit"` + RequiredNumberOfVerifiedSignaturesOnCommit int `structs:"required_number_of_verified_signatures_on_commit" json:"required_number_of_verified_signatures_on_commit"` + S3Endpoint string `structs:"s3_endpoint" json:"s3_endpoint"` + S3Region string `structs:"s3_region" json:"s3_region"` + S3AccessKeyID string `structs:"s3_access_key_id" json:"s3_access_key_id"` + S3SecretAccessKey string `structs:"s3_secret_access_key" json:"s3_secret_access_key"` + S3BucketName string `structs:"s3_bucket_name" json:"s3_bucket_name"` + BuildxDriver string `structs:"buildx_driver" json:"buildx_driver"` + BuildxDriverOpts []string `structs:"buildx_driver_opts" json:"buildx_driver_opts"` } func (cfg *configuration) RepositoryOptions() publisher.RepositoryOptions { diff --git a/server/path_configure_ai_test.go b/server/path_configure_ai_test.go new file mode 100644 index 00000000..9b76eeb6 --- /dev/null +++ b/server/path_configure_ai_test.go @@ -0,0 +1,90 @@ +//go:build ai_tests + +package server + +import ( + "github.com/hashicorp/vault/sdk/logical" + "github.com/stretchr/testify/assert" +) + +func (suite *PathConfigureCallbacksSuite) TestAI_CreateOrUpdate_BuildxFieldsOmitted() { + reqData := dataCompleteConfiguration() + delete(reqData, fieldNameBuildxDriver) + delete(reqData, fieldNameBuildxDriverOpts) + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), resp) + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), cfg) { + assert.Empty(suite.T(), cfg.BuildxDriver) + assert.Empty(suite.T(), cfg.BuildxDriverOpts) + } +} + +func (suite *PathConfigureCallbacksSuite) TestAI_CreateOrUpdate_UnsupportedBuildxDriver() { + reqData := dataCompleteConfiguration() + reqData[fieldNameBuildxDriver] = "docker" + + suite.req.Operation = logical.CreateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), resp) { + assert.Contains(suite.T(), resp.Error().Error(), fieldNameBuildxDriver) + } + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Nil(suite.T(), cfg) +} + +func (suite *PathConfigureCallbacksSuite) TestAI_CreateOrUpdate_RejectedUpdateKeepsConfiguration() { + err := putConfiguration(suite.ctx, suite.storage, completeConfiguration()) + assert.Nil(suite.T(), err) + + reqData := dataCompleteConfiguration() + reqData[fieldNameBuildxDriver] = "docker" + + suite.req.Operation = logical.UpdateOperation + suite.req.Data = reqData + + resp, err := suite.backend.HandleRequest(suite.ctx, suite.req) + assert.Nil(suite.T(), err) + assert.NotNil(suite.T(), resp) + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + assert.Equal(suite.T(), completeConfiguration(), cfg) +} + +// A configuration written before the buildx fields existed carries neither key. +func (suite *PathConfigureCallbacksSuite) TestAI_Read_ConfigurationStoredBeforeBuildxFields() { + entry := &logical.StorageEntry{ + Key: storageKeyConfiguration, + Value: []byte(`{ + "git_repo_url": "https://github.com/werf/trdl/server.git", + "required_number_of_verified_signatures_on_commit": 10, + "s3_endpoint": "trdl.s3.us-west-2.example.com", + "s3_region": "us-west-2", + "s3_access_key_id": "AKIAIOSFODNN7EXAMPLE", + "s3_secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "s3_bucket_name": "trdl" + }`), + } + assert.Nil(suite.T(), suite.storage.Put(suite.ctx, entry)) + + cfg, err := getConfiguration(suite.ctx, suite.storage) + assert.Nil(suite.T(), err) + if assert.NotNil(suite.T(), cfg) { + assert.Empty(suite.T(), cfg.BuildxDriver) + assert.Empty(suite.T(), cfg.BuildxDriverOpts) + assert.Equal(suite.T(), "trdl", cfg.S3BucketName) + } +} diff --git a/server/path_configure_test.go b/server/path_configure_test.go index 02ad6fa4..afb0fd10 100644 --- a/server/path_configure_test.go +++ b/server/path_configure_test.go @@ -124,6 +124,8 @@ func dataCompleteConfiguration() map[string]interface{} { fieldNameS3AccessKeyID: cfg.S3AccessKeyID, fieldNameS3SecretAccessKey: cfg.S3SecretAccessKey, fieldNameS3BucketName: cfg.S3BucketName, + fieldNameBuildxDriver: cfg.BuildxDriver, + fieldNameBuildxDriverOpts: cfg.BuildxDriverOpts, } } @@ -138,5 +140,7 @@ func completeConfiguration() *configuration { S3AccessKeyID: "AKIAIOSFODNN7EXAMPLE", S3SecretAccessKey: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", S3BucketName: "trdl", + BuildxDriver: "kubernetes", + BuildxDriverOpts: []string{"namespace=trdl-build", "nodeselector=disktype=ssd,zone=a"}, } } diff --git a/server/path_release.go b/server/path_release.go index d32a3e4c..4ca94da6 100644 --- a/server/path_release.go +++ b/server/path_release.go @@ -165,11 +165,13 @@ func (b *Backend) pathRelease(ctx context.Context, req *logical.Request, fields go func() { err := docker.BuildReleaseArtifacts(ctx, docker.BuildReleaseArtifactsOpts{ - TarWriter: tarWriter, - GitRepo: gitRepo, - FromImage: trdlCfg.GetDockerImage(), - RunCommands: trdlCfg.Commands, - Storage: req.Storage, + TarWriter: tarWriter, + GitRepo: gitRepo, + FromImage: trdlCfg.GetDockerImage(), + RunCommands: trdlCfg.Commands, + Storage: req.Storage, + BuildxDriver: cfg.BuildxDriver, + BuildxDriverOpts: cfg.BuildxDriverOpts, }, b.Logger()) if err != nil { errCh <- err diff --git a/server/pkg/docker/build.go b/server/pkg/docker/build.go index ee08c7ee..ced73039 100644 --- a/server/pkg/docker/build.go +++ b/server/pkg/docker/build.go @@ -25,11 +25,13 @@ const ( ) type BuildReleaseArtifactsOpts struct { - FromImage string - RunCommands []string - GitRepo *git.Repository - TarWriter *nio.PipeWriter - Storage logical.Storage + FromImage string + RunCommands []string + GitRepo *git.Repository + TarWriter *nio.PipeWriter + Storage logical.Storage + BuildxDriver string + BuildxDriverOpts []string } func BuildReleaseArtifacts(ctx context.Context, opts BuildReleaseArtifactsOpts, logger hclog.Logger) error { @@ -100,6 +102,8 @@ func BuildReleaseArtifacts(ctx context.Context, opts BuildReleaseArtifactsOpts, builder, err := NewBuilder(ctx, &NewBuilderOpts{ BuildId: buildId, ContextPath: serviceDockerfilePathInContext, + BuildxDriver: opts.BuildxDriver, + BuildxDriverOpts: opts.BuildxDriverOpts, Secrets: secrets, MacSigningCredentials: credentials, Logger: logger, diff --git a/server/pkg/docker/build_ai_test.go b/server/pkg/docker/build_ai_test.go new file mode 100644 index 00000000..37415e2e --- /dev/null +++ b/server/pkg/docker/build_ai_test.go @@ -0,0 +1,44 @@ +//go:build ai_tests + +package docker + +import ( + "context" + "testing" + + "github.com/djherbis/buffer" + "github.com/djherbis/nio/v3" + "github.com/go-git/go-billy/v5/memfs" + "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/storage/memory" + "github.com/hashicorp/go-hclog" + "github.com/hashicorp/vault/sdk/logical" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// The build stops at builder creation, before any docker invocation, so the +// error proves that the configured driver traveled from the release options +// all the way into the buildx arguments. +func TestAI_BuildReleaseArtifacts_ForwardsConfiguredDriver(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "kubernetes") + + gitRepo, err := git.Init(memory.NewStorage(), memfs.New()) + require.NoError(t, err) + + _, tarWriter := nio.Pipe(buffer.New(1024)) + + err = BuildReleaseArtifacts(context.Background(), BuildReleaseArtifactsOpts{ + FromImage: "alpine", + RunCommands: []string{"true"}, + GitRepo: gitRepo, + TarWriter: tarWriter, + Storage: &logical.InmemStorage{}, + BuildxDriver: "docker", + }, hclog.NewNullLogger()) + + require.Error(t, err) + assert.Contains(t, err.Error(), buildxDriverConfigurationSource) + assert.Contains(t, err.Error(), `"docker"`) +} diff --git a/server/pkg/docker/builder.go b/server/pkg/docker/builder.go index 9609c67d..200a2079 100644 --- a/server/pkg/docker/builder.go +++ b/server/pkg/docker/builder.go @@ -27,6 +27,8 @@ const ( buildxDriverOptsEnvPrefix = "TRDL_BUILDX_DRIVER_OPTS_" buildxDriverOptsSeparatorEnv = "TRDL_BUILDX_DRIVER_OPTS_SEPARATOR" + buildxDriverConfigurationSource = "the buildx_driver plugin configuration" + defaultBuildxDriver = "docker-container" ) @@ -50,6 +52,8 @@ type Builder struct { type NewBuilderOpts struct { BuildId string ContextPath string + BuildxDriver string + BuildxDriverOpts []string Secrets []secrets.Secret MacSigningCredentials *mac_signing.Credentials Logger Logger @@ -58,7 +62,7 @@ type NewBuilderOpts struct { func NewBuilder(ctx context.Context, opts *NewBuilderOpts) (*Builder, error) { builderName := fmt.Sprintf("trdl-builder-%s", opts.BuildId) - builderArgs, err := buildxCreateArgs(builderName) + builderArgs, err := buildxCreateArgs(builderName, opts.BuildxDriver, opts.BuildxDriverOpts) if err != nil { return nil, fmt.Errorf("unable to construct buildx create args: %w", err) } @@ -79,13 +83,10 @@ func NewBuilder(ctx context.Context, opts *NewBuilderOpts) (*Builder, error) { }, nil } -func buildxCreateArgs(builderName string) ([]string, error) { - driver := strings.TrimSpace(os.Getenv(buildxDriverEnv)) - if driver == "" { - driver = defaultBuildxDriver - } - if !lo.Contains(supportedBuildxDrivers, driver) { - return nil, fmt.Errorf("unsupported buildx driver %q from %s (supported: %s)", driver, buildxDriverEnv, strings.Join(supportedBuildxDrivers, ", ")) +func buildxCreateArgs(builderName, configuredDriver string, configuredDriverOpts []string) ([]string, error) { + driver, driverSource := resolveBuildxDriver(configuredDriver) + if err := ValidateBuildxDriver(driver); err != nil { + return nil, fmt.Errorf("buildx driver from %s: %w", driverSource, err) } args := []string{ @@ -94,13 +95,55 @@ func buildxCreateArgs(builderName string) ([]string, error) { "--name", builderName, "--driver=" + driver, } - for _, opt := range driverOptsFromEnv() { + for _, opt := range resolveBuildxDriverOpts(configuredDriverOpts) { args = append(args, "--driver-opt="+opt) } return args, nil } +// ValidateBuildxDriver accepts an empty driver, meaning the setting is not in +// use. The build streams a tarball to stdout (`-o - -`), which the default +// "docker" driver cannot export, so an unsupported driver is rejected here +// instead of failing opaquely mid-build. +func ValidateBuildxDriver(driver string) error { + driver = strings.TrimSpace(driver) + if driver == "" || lo.Contains(supportedBuildxDrivers, driver) { + return nil + } + + return fmt.Errorf("unsupported driver %q (supported: %s)", driver, strings.Join(supportedBuildxDrivers, ", ")) +} + +// resolveBuildxDriver returns the driver to create the builder with and the +// name of the setting it came from, so that a rejection points at the knob the +// operator has to fix. +func resolveBuildxDriver(configuredDriver string) (string, string) { + if driver := strings.TrimSpace(configuredDriver); driver != "" { + return driver, buildxDriverConfigurationSource + } + if driver := strings.TrimSpace(os.Getenv(buildxDriverEnv)); driver != "" { + return driver, buildxDriverEnv + } + + return defaultBuildxDriver, "the default" +} + +// An empty configured list means the setting is not in use, not "run with no +// options": `configure` cannot tell an omitted field from an explicitly empty +// one, so both fall back to the environment. +func resolveBuildxDriverOpts(configuredDriverOpts []string) []string { + var opts []string + for _, configuredOpt := range configuredDriverOpts { + opts = append(opts, parseDriverOpts(configuredOpt, "")...) + } + if len(opts) > 0 { + return opts + } + + return driverOptsFromEnv() +} + func driverOptsFromEnv() []string { var names []string for _, keyValue := range os.Environ() { diff --git a/server/pkg/docker/builder_ai_test.go b/server/pkg/docker/builder_ai_test.go new file mode 100644 index 00000000..1d741f57 --- /dev/null +++ b/server/pkg/docker/builder_ai_test.go @@ -0,0 +1,111 @@ +//go:build ai_tests + +package docker + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAI_BuildxCreateArgs_ConfigurationOverridesEnv(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "docker-container") + t.Setenv(buildxDriverOptsEnvPrefix+"IMAGE", "image=moby/buildkit:v0.12.0") + + args, err := buildxCreateArgs("trdl-builder-42", "kubernetes", []string{"namespace=trdl-build", "rootless=true"}) + + require.NoError(t, err) + assert.Equal(t, []string{ + "buildx", "create", + "--name", "trdl-builder-42", + "--driver=kubernetes", + "--driver-opt=namespace=trdl-build", + "--driver-opt=rootless=true", + }, args) +} + +func TestAI_BuildxCreateArgs_EnvUsedWhenConfigurationEmpty(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "kubernetes") + t.Setenv(buildxDriverOptsEnvPrefix+"NAMESPACE", "namespace=trdl-build") + + args, err := buildxCreateArgs("trdl-builder-42", "", nil) + + require.NoError(t, err) + assert.Equal(t, []string{ + "buildx", "create", + "--name", "trdl-builder-42", + "--driver=kubernetes", + "--driver-opt=namespace=trdl-build", + }, args) +} + +func TestAI_BuildxCreateArgs_ConfiguredDriverKeepsEnvOpts(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "") + t.Setenv(buildxDriverOptsEnvPrefix+"NAMESPACE", "namespace=trdl-build") + + args, err := buildxCreateArgs("trdl-builder-42", "kubernetes", nil) + + require.NoError(t, err) + assert.Equal(t, []string{ + "buildx", "create", + "--name", "trdl-builder-42", + "--driver=kubernetes", + "--driver-opt=namespace=trdl-build", + }, args) +} + +func TestAI_BuildxCreateArgs_ConfiguredOptsPassedThroughAndTrimmed(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "") + + args, err := buildxCreateArgs("trdl-builder-42", " kubernetes ", []string{" nodeselector=disktype=ssd,zone=a ", " "}) + + require.NoError(t, err) + assert.Equal(t, []string{ + "buildx", "create", + "--name", "trdl-builder-42", + "--driver=kubernetes", + "--driver-opt=nodeselector=disktype=ssd,zone=a", + }, args) +} + +func TestAI_BuildxCreateArgs_UnsupportedConfiguredDriverRejected(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "kubernetes") + + args, err := buildxCreateArgs("trdl-builder-42", "docker", nil) + + require.Error(t, err) + assert.Nil(t, args) + assert.Contains(t, err.Error(), `"docker"`) + assert.Contains(t, err.Error(), buildxDriverConfigurationSource) + assert.NotContains(t, err.Error(), buildxDriverEnv) +} + +func TestAI_NewBuilder_UsesConfiguredDriver(t *testing.T) { + clearDriverOptsEnv(t) + t.Setenv(buildxDriverEnv, "kubernetes") + + builder, err := NewBuilder(context.Background(), &NewBuilderOpts{ + BuildId: "42", + BuildxDriver: "docker", + }) + + require.Error(t, err) + assert.Nil(t, builder) + assert.Contains(t, err.Error(), buildxDriverConfigurationSource) +} + +func TestAI_ValidateBuildxDriver(t *testing.T) { + assert.NoError(t, ValidateBuildxDriver("")) + assert.NoError(t, ValidateBuildxDriver(" ")) + assert.NoError(t, ValidateBuildxDriver(" kubernetes ")) + assert.NoError(t, ValidateBuildxDriver("docker-container")) + assert.Error(t, ValidateBuildxDriver("docker")) + assert.Error(t, ValidateBuildxDriver("remote")) +} diff --git a/server/pkg/docker/builder_test.go b/server/pkg/docker/builder_test.go index 6b3f016b..1f0dbea8 100644 --- a/server/pkg/docker/builder_test.go +++ b/server/pkg/docker/builder_test.go @@ -23,7 +23,7 @@ func TestBuildxCreateArgs_DefaultDriverUnchanged(t *testing.T) { clearDriverOptsEnv(t) t.Setenv(buildxDriverEnv, "") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{ @@ -39,7 +39,7 @@ func TestBuildxCreateArgs_KubernetesDriverWithOpts(t *testing.T) { t.Setenv(buildxDriverOptsEnvPrefix+"NAMESPACE", "namespace=trdl-build") t.Setenv(buildxDriverOptsEnvPrefix+"ROOTLESS", "rootless=true") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{ @@ -57,7 +57,7 @@ func TestBuildxCreateArgs_DefaultDriverWithOpts(t *testing.T) { t.Setenv(buildxDriverOptsEnvPrefix+"IMAGE", "image=moby/buildkit:v0.12.0") t.Setenv(buildxDriverOptsEnvPrefix+"NETWORK", "network=host") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{ @@ -73,7 +73,7 @@ func TestBuildxCreateArgs_DriverValueTrimmed(t *testing.T) { clearDriverOptsEnv(t) t.Setenv(buildxDriverEnv, " kubernetes ") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Contains(t, args, "--driver=kubernetes") @@ -83,7 +83,7 @@ func TestBuildxCreateArgs_UnsupportedDriverRejected(t *testing.T) { clearDriverOptsEnv(t) t.Setenv(buildxDriverEnv, "docker") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.Error(t, err) assert.Nil(t, args) @@ -96,7 +96,7 @@ func TestBuildxCreateArgs_CommaValuePassedThroughWithoutSeparator(t *testing.T) t.Setenv(buildxDriverEnv, "kubernetes") t.Setenv(buildxDriverOptsEnvPrefix+"NODESELECTOR", "nodeselector=disktype=ssd,zone=a") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{ @@ -113,7 +113,7 @@ func TestBuildxCreateArgs_CustomOptsSeparator(t *testing.T) { t.Setenv(buildxDriverOptsSeparatorEnv, ";") t.Setenv(buildxDriverOptsEnvPrefix+"KUBE", "namespace=trdl-build;nodeselector=disktype=ssd,zone=a") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{ @@ -132,7 +132,7 @@ func TestBuildxCreateArgs_OptsOrderedByVariableName(t *testing.T) { t.Setenv(buildxDriverOptsEnvPrefix+"A", "first=1") t.Setenv(buildxDriverOptsEnvPrefix+"EMPTY", " ") - args, err := buildxCreateArgs("trdl-builder-42") + args, err := buildxCreateArgs("trdl-builder-42", "", nil) require.NoError(t, err) assert.Equal(t, []string{