diff --git a/backend/provisioner/oci_image_provisioner.go b/backend/provisioner/oci_image_provisioner.go index 6ae497ecc8..a895213439 100644 --- a/backend/provisioner/oci_image_provisioner.go +++ b/backend/provisioner/oci_image_provisioner.go @@ -17,13 +17,17 @@ import ( "github.com/block/ftl/internal/oci" ) -func NewOCIImageProvisioner(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string) *InMemProvisioner { +type OCIImageProvisionerConfig struct { + Env map[string]string `toml:"env"` +} + +func NewOCIImageProvisioner(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, cfg OCIImageProvisionerConfig) *InMemProvisioner { return NewEmbeddedProvisioner(map[schema.ResourceType]InMemResourceProvisionerFn{ - schema.ResourceTypeImage: provisionOCIImage(storage, astorage, defaultImage), + schema.ResourceTypeImage: provisionOCIImage(storage, astorage, defaultImage, cfg), }, map[schema.ResourceType]InMemResourceProvisionerFn{}) } -func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string) InMemResourceProvisionerFn { +func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, defaultImage string, cfg OCIImageProvisionerConfig) InMemResourceProvisionerFn { return func(ctx context.Context, changeset key.Changeset, deployment key.Deployment, rc schema.Provisioned, moduleSch *schema.Module) (*schema.RuntimeElement, error) { logger := log.FromContext(ctx) variants := goslices.Collect(slices.FilterVariants[*schema.MetadataArtefact](moduleSch.Metadata)) @@ -56,7 +60,7 @@ func provisionOCIImage(storage *oci.ImageService, astorage *oci.ArtefactService, } target := storage.Image(deployment.Payload.Realm, deployment.Payload.Module, tag) - err = storage.BuildOCIImageFromRemote(ctx, astorage, image, target, tempDir, moduleSch, deployment, variants, oci.WithRemotePush()) + err = storage.BuildOCIImageFromRemote(ctx, astorage, image, target, tempDir, moduleSch, deployment, variants, cfg.Env, oci.WithRemotePush()) if err != nil { return nil, errors.Wrap(err, "failed to build image") } diff --git a/backend/provisioner/registry.go b/backend/provisioner/registry.go index b6c8429e15..f2683ea261 100644 --- a/backend/provisioner/registry.go +++ b/backend/provisioner/registry.go @@ -5,6 +5,7 @@ import ( "fmt" "strings" + "github.com/BurntSushi/toml" "github.com/alecthomas/errors" "github.com/block/ftl/backend/protos/xyz/block/ftl/admin/v1/adminpbconnect" @@ -25,6 +26,7 @@ type provisionerPluginConfig struct { Plugins []struct { ID string `toml:"id"` Resources []schema.ResourceType `toml:"resources"` + Config *toml.Primitive `toml:"config"` } `toml:"plugins"` } @@ -70,7 +72,7 @@ func (reg *ProvisionerRegistry) listBindings() []*ProvisionerBinding { type pluginProcesses map[string]Plugin -func registryFromConfig(ctx context.Context, workingDir string, cfg *provisionerPluginConfig, runnerScaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService) (*ProvisionerRegistry, error) { +func registryFromConfig(ctx context.Context, workingDir string, cfg *provisionerPluginConfig, md *toml.MetaData, runnerScaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, imageService *oci.ImageService, artifactService *oci.ArtefactService) (*ProvisionerRegistry, error) { logger := log.FromContext(ctx) result := &ProvisionerRegistry{} if err := cfg.Validate(); err != nil { @@ -78,7 +80,7 @@ func registryFromConfig(ctx context.Context, workingDir string, cfg *provisioner } processes := pluginProcesses{} for _, plugin := range cfg.Plugins { - provisioner, err := provisionerIDToProvisioner(ctx, plugin.ID, workingDir, runnerScaling, adminClient, imageService, artifactService, processes) + provisioner, err := provisionerIDToProvisioner(ctx, plugin.ID, plugin.Config, md, workingDir, runnerScaling, adminClient, imageService, artifactService, processes) if err != nil { return nil, errors.WithStack(err) } @@ -91,6 +93,8 @@ func registryFromConfig(ctx context.Context, workingDir string, cfg *provisioner func provisionerIDToProvisioner( ctx context.Context, id string, + config *toml.Primitive, + md *toml.MetaData, workingDir string, scaling scaling.RunnerScaling, adminClient adminpbconnect.AdminServiceClient, @@ -108,7 +112,14 @@ func provisionerIDToProvisioner( case "noop": return NewPluginClient(&NoopProvisioner{}), nil case "oci-image": - return NewOCIImageProvisioner(imageService, artifactService, "ftl0/ftl-runner"), nil + cfg := OCIImageProvisionerConfig{} + if config != nil { + if err := md.PrimitiveDecode(*config, &cfg); err != nil { + return nil, errors.Wrap(err, "error unmarshalling oci-image provisioner config") + } + } + + return NewOCIImageProvisioner(imageService, artifactService, "ftl0/ftl-runner", cfg), nil default: if _, ok := processes[id]; ok { return processes[id], nil diff --git a/backend/provisioner/service.go b/backend/provisioner/service.go index 17b1f7bd32..1f392f5269 100644 --- a/backend/provisioner/service.go +++ b/backend/provisioner/service.go @@ -204,11 +204,13 @@ func RegistryFromConfigFile(ctx context.Context, workingDir string, file *os.Fil if err != nil { return nil, errors.Wrapf(err, "error reading plugin configuration from %s", file.Name()) } - if err := toml.Unmarshal(bytes, &config); err != nil { + + md, err := toml.Decode(string(bytes), &config) + if err != nil { return nil, errors.Wrap(err, "error parsing plugin configuration") } - registry, err := registryFromConfig(ctx, workingDir, &config, scaling, adminClient, imageService, artifactService) + registry, err := registryFromConfig(ctx, workingDir, &config, &md, scaling, adminClient, imageService, artifactService) if err != nil { return nil, errors.Wrap(err, "error creating provisioner registry") } diff --git a/cmd/ftl-provisioner/main.go b/cmd/ftl-provisioner/main.go index fa1fdfb4e4..6dc8d62b8d 100644 --- a/cmd/ftl-provisioner/main.go +++ b/cmd/ftl-provisioner/main.go @@ -82,7 +82,7 @@ func main() { if _, ok := slices.Find(registry.Bindings, func(binding *provisioner.ProvisionerBinding) bool { return slices.Contains(binding.Types, schema.ResourceTypeImage) }); !ok { - ociProvisioner := provisioner.NewOCIImageProvisioner(imageService, artefactService, cli.DefaultRunnerImage) + ociProvisioner := provisioner.NewOCIImageProvisioner(imageService, artefactService, cli.DefaultRunnerImage, provisioner.OCIImageProvisionerConfig{}) runnerBinding := registry.Register("oci-image", ociProvisioner, schema.ResourceTypeImage) logger.Debugf("Registered provisioner %s as fallback for image", runnerBinding) } diff --git a/cmd/ftl/cmd_image_build.go b/cmd/ftl/cmd_image_build.go index bb0da128e5..394bc3af07 100644 --- a/cmd/ftl/cmd_image_build.go +++ b/cmd/ftl/cmd_image_build.go @@ -115,7 +115,7 @@ func (b *imageBuildCmd) Run( // TODO: we need to properly sync the deployment with the actual deployment key // this is just a hack to get the module and realm to the runner deployment := key.NewDeploymentKey(projConfig.Name, moduleSch.Name) - err := imageService.BuildOCIImage(ctx, image, tgt, tmpDeployDir, deployment, artifacts, targets...) + err := imageService.BuildOCIImage(ctx, image, tgt, tmpDeployDir, deployment, artifacts, nil, targets...) if err != nil { return errors.Wrapf(err, "failed to build image") } diff --git a/internal/oci/image_service.go b/internal/oci/image_service.go index d64f3004c9..a09f5a8923 100644 --- a/internal/oci/image_service.go +++ b/internal/oci/image_service.go @@ -152,6 +152,7 @@ func (s *ImageService) BuildOCIImageFromRemote( module *schema.Module, deployment key.Deployment, artifacts []*schema.MetadataArtefact, + envVars map[string]string, targets ...ImageTarget, ) error { target, err := os.MkdirTemp(tempDir, "ftl-image-") @@ -194,7 +195,7 @@ func (s *ImageService) BuildOCIImageFromRemote( if err != nil { return errors.Wrapf(err, "failed to download artifacts") } - return s.BuildOCIImage(ctx, baseImage, targetImage, target, deployment, artifacts, targets...) + return s.BuildOCIImage(ctx, baseImage, targetImage, target, deployment, artifacts, envVars, targets...) } @@ -205,6 +206,7 @@ func (s *ImageService) BuildOCIImage( apath string, deployment key.Deployment, allArtifacts []*schema.MetadataArtefact, + envVars map[string]string, targets ...ImageTarget, ) error { var artifacts []*schema.MetadataArtefact @@ -281,6 +283,9 @@ func (s *ImageService) BuildOCIImage( cfg.Config.Env = append(cfg.Config.Env, fmt.Sprintf("FTL_DEPLOYMENT=%s", deployment.String())) cfg.Config.Env = append(cfg.Config.Env, "LOG_LEVEL=DEBUG") cfg.Config.Env = append(cfg.Config.Env, "LOG_FORMAT=json") + for k, v := range envVars { + cfg.Config.Env = append(cfg.Config.Env, fmt.Sprintf("%s=%s", k, v)) + } cfg.Config.Labels[SchemaLabel] = schDigest.String() newImg, err = mutate.Config(newImg, cfg.Config) if err != nil {