diff --git a/.gitignore b/.gitignore index 9b8fb2e61d..a7e650233d 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ # MacOS .DS_Store + +runtimes/supervisor-encore diff --git a/Cargo.lock b/Cargo.lock index 22cde99082..756bfd19d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,12 +85,55 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.76" @@ -991,6 +1034,12 @@ dependencies = [ "cc", ] +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + [[package]] name = "colored" version = "2.1.0" @@ -1418,6 +1467,25 @@ dependencies = [ "xid", ] +[[package]] +name = "encore-supervisor" +version = "0.1.0" +dependencies = [ + "base64 0.21.5", + "env_logger 0.11.5", + "flate2", + "libc", + "log", + "prost", + "prost-build", + "prost-types", + "serde", + "serde_json", + "tokio", + "tokio-retry", + "tokio-util", +] + [[package]] name = "encore-tsparser" version = "0.1.0" @@ -1460,6 +1528,16 @@ dependencies = [ "walkdir", ] +[[package]] +name = "env_filter" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f2c92ceda6ceec50f43169f9ee8424fe2db276791afde7b2cd8bc084cb376ab" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -1483,6 +1561,19 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13fa619b91fb2381732789fc5de83b45675e882f66623b7d8cb4f643017018d" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -1964,9 +2055,9 @@ dependencies = [ [[package]] name = "hermit-abi" -version = "0.3.3" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "hex" @@ -2342,11 +2433,17 @@ version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "rustix", "windows-sys 0.48.0", ] +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + [[package]] name = "itertools" version = "0.10.5" @@ -2438,9 +2535,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.151" +version = "0.2.158" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" [[package]] name = "libloading" @@ -2520,9 +2617,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" dependencies = [ "serde", "value-bag", @@ -2628,13 +2725,14 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.10" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" dependencies = [ + "hermit-abi 0.3.9", "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -2835,7 +2933,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.3.3", + "hermit-abi 0.3.9", "libc", ] @@ -3335,7 +3433,7 @@ dependencies = [ "pingora-http", "pingora-ketama", "pingora-runtime", - "rand 0.4.6", + "rand 0.8.5", "tokio", ] @@ -3347,7 +3445,7 @@ dependencies = [ "arrayvec", "hashbrown 0.14.3", "parking_lot", - "rand 0.4.6", + "rand 0.8.5", ] [[package]] @@ -5234,21 +5332,20 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "bytes", "libc", "mio", - "num_cpus", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys 0.52.0", ] [[package]] @@ -5263,9 +5360,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.2.0" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", @@ -5419,16 +5516,17 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.10" +version = "0.7.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" dependencies = [ "bytes", "futures-core", "futures-sink", + "futures-util", + "hashbrown 0.14.3", "pin-project-lite", "tokio", - "tracing", ] [[package]] @@ -5798,6 +5896,12 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + [[package]] name = "uuid" version = "1.7.0" @@ -5816,9 +5920,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" [[package]] name = "value-bag" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cdbaf5e132e593e9fc1de6a15bbec912395b11fb9719e061cf64f804524c503" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" dependencies = [ "value-bag-serde1", "value-bag-sval2", @@ -5826,9 +5930,9 @@ dependencies = [ [[package]] name = "value-bag-serde1" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92cad98b1b18d06b6f38b3cd04347a9d7a3a0111441a061f71377fb6740437e4" +checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b" dependencies = [ "erased-serde", "serde", @@ -5837,9 +5941,9 @@ dependencies = [ [[package]] name = "value-bag-sval2" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dc7271d6b3bf58dd2e610a601c0e159f271ffdb7fbb21517c40b52138d64f8e" +checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b" dependencies = [ "sval", "sval_buffer", diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 0000000000..4a264df650 --- /dev/null +++ b/Cross.toml @@ -0,0 +1,12 @@ +[build] +pre-build = [ + "apt-get install unzip &&", + "curl -LO https://github.com/protocolbuffers/protobuf/releases/download/v24.4/protoc-24.4-linux-x86_64.zip &&", + "unzip protoc-24.4-linux-x86_64.zip -d /usr/local &&", + "rm protoc-24.4-linux-x86_64.zip &&", + "export PATH=$PATH:/usr/local/bin", +] + +[build.env] +volumes = ["ENCORE_WORKDIR"] +passthrough = ["TYPE_DEF_TMP_PATH", "ENCORE_VERSION"] \ No newline at end of file diff --git a/cli/cmd/encore/eject.go b/cli/cmd/encore/eject.go index f8666c2be7..9b0d88e033 100644 --- a/cli/cmd/encore/eject.go +++ b/cli/cmd/encore/eject.go @@ -5,10 +5,10 @@ import ( "fmt" "os" "os/signal" - "runtime" "github.com/spf13/cobra" + "encr.dev/pkg/appfile" daemonpb "encr.dev/proto/encore/daemon" ) @@ -21,7 +21,7 @@ func init() { p := ejectParams{ CgoEnabled: os.Getenv("CGO_ENABLED") == "1", Goos: or(os.Getenv("GOOS"), "linux"), - Goarch: or(os.Getenv("GOARCH"), runtime.GOARCH), + Goarch: or(os.Getenv("GOARCH"), "amd64"), } dockerEjectCmd := &cobra.Command{ Use: "docker IMAGE_TAG", @@ -34,8 +34,20 @@ func init() { }, } + lang, err := appfile.AppLang(p.AppRoot) + if err != nil { + lang = appfile.LangGo + } + p.BaseImg = "scratch" + if lang == appfile.LangTS { + p.BaseImg = "node" + } + dockerEjectCmd.Flags().BoolVarP(&p.Push, "push", "p", false, "push image to remote repository") - dockerEjectCmd.Flags().StringVar(&p.BaseImg, "base", "scratch", "base image to build from") + dockerEjectCmd.Flags().StringVar(&p.BaseImg, "base", p.BaseImg, "base image to build from") + dockerEjectCmd.Flags().StringVar(&p.Goos, "os", p.Goos, "target operating system. One of (linux, darwin, windows)") + dockerEjectCmd.Flags().StringVar(&p.Goarch, "arch", p.Goarch, "target architecture. One of (amd64, arm64)") + dockerEjectCmd.Flags().BoolVar(&p.CgoEnabled, "cgo", p.CgoEnabled, "enable cgo") rootCmd.AddCommand(ejectCmd) ejectCmd.AddCommand(dockerEjectCmd) } diff --git a/cli/daemon/apps/apps.go b/cli/daemon/apps/apps.go index 6bbf24dc50..9874736045 100644 --- a/cli/daemon/apps/apps.go +++ b/cli/daemon/apps/apps.go @@ -359,6 +359,14 @@ func (i *Instance) Lang() appfile.Lang { return appFile.Lang } +func (i *Instance) ProcessPerService() bool { + appFile, err := appfile.ParseFile(filepath.Join(i.root, appfile.Name)) + if err != nil { + return false + } + return appFile.Build.Docker.ProcessPerService +} + // GlobalCORS returns the CORS configuration for the app which // will be applied against all API gateways into the app func (i *Instance) GlobalCORS() (appfile.CORS, error) { diff --git a/cli/daemon/export/download.go b/cli/daemon/export/download.go new file mode 100644 index 0000000000..68f2091dae --- /dev/null +++ b/cli/daemon/export/download.go @@ -0,0 +1,100 @@ +package export + +import ( + "fmt" + "io" + "net/http" + "os" + "path/filepath" + "runtime" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "encr.dev/internal/conf" + "encr.dev/internal/env" + "encr.dev/internal/version" + "encr.dev/pkg/dockerbuild" +) + +const ( + DOWNLOAD_BASE_URL = "https://storage.googleapis.com/encore-optional/encore" +) + +func downloadBinary(platform, arch string, binary string, log zerolog.Logger) (dockerbuild.HostPath, error) { + if version.Channel == version.DevBuild { + suffix := "" + if platform != runtime.GOOS || arch != runtime.GOARCH { + suffix = "-" + platform + "-" + arch + } + path := filepath.Join(env.EncoreRuntimesPath(), binary+suffix) + if _, err := os.Stat(path); err == nil { + return dockerbuild.HostPath(path), nil + } + return "", fmt.Errorf("development build of %s/%s %s not found at %s. Build it with `go run ./pkg/encorebuild/cmd/build-local-binary %[3]s --os=%[1]s --arch=%[2]s`", platform, arch, binary, path) + } + cacheDir, err := conf.CacheDir() + if err != nil { + return "", err + } + binDir := dockerbuild.HostPath(cacheDir).Join("bin") + archDir := binDir.Join(version.Version, platform, arch) + binaryPath := archDir.Join(binary) + if _, err := os.Stat(binaryPath.String()); err == nil { + return binaryPath, nil + } + if err := os.MkdirAll(archDir.String(), 0755); err != nil { + return "", err + } + // Download the binaries + archURL := fmt.Sprintf("%s/%s/%s-%s", DOWNLOAD_BASE_URL, version.Version, platform, arch) + url := fmt.Sprintf("%s/%s", archURL, binary) + log.Info().Msgf("Downloading %s/%s %s", platform, arch, binary) + if err := downloadFile(url, binaryPath.String()); err != nil { + return "", err + } + tryCleanupPreviousVersions(binDir) + return binaryPath, nil +} + +func tryCleanupPreviousVersions(binDir dockerbuild.HostPath) { + // Clean up binaries for other versions + entries, err := os.ReadDir(binDir.String()) + if err != nil { + log.Warn().Msgf("failed to read directory %s: %v", binDir, err) + return + } + for _, entry := range entries { + if entry.IsDir() && entry.Name() != version.Version { + oldVersionPath := filepath.Join(binDir.String(), entry.Name()) + if err := os.RemoveAll(oldVersionPath); err != nil { + log.Warn().Msgf("failed to remove old version directory %s: %v", oldVersionPath, err) + } + } + } + return +} + +func downloadFile(url, dest string) error { + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download %s: %s", url, resp.Status) + } + + out, err := os.OpenFile(dest, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) + if err != nil { + return err + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + if err != nil { + return err + } + return nil +} diff --git a/cli/daemon/export/export.go b/cli/daemon/export/export.go index 0122a29f8c..bbb71267bc 100644 --- a/cli/daemon/export/export.go +++ b/cli/daemon/export/export.go @@ -15,6 +15,7 @@ import ( "encr.dev/cli/daemon/apps" "encr.dev/internal/env" + "encr.dev/pkg/appfile" "encr.dev/pkg/builder" "encr.dev/pkg/builder/builderimpl" "encr.dev/pkg/cueutil" @@ -98,12 +99,23 @@ func Docker(ctx context.Context, app *apps.Instance, req *daemonpb.ExportRequest return false, errors.Wrap(err, "compilation failed") } + var crossNodeRuntime option.Option[dockerbuild.HostPath] + if app.Lang() == appfile.LangTS && buildInfo.IsCrossBuild() { + binary, err := downloadBinary(req.Goos, req.Goarch, "encore-runtime.node", log) + if err != nil { + return false, errors.Wrap(err, "download runtime binaries") + } + crossNodeRuntime = option.Some(binary) + } + spec, err := dockerbuild.Describe(dockerbuild.DescribeConfig{ - Meta: parse.Meta, - Compile: result, - BundleSource: option.Option[dockerbuild.BundleSourceSpec]{}, - DockerBaseImage: option.AsOptional(params.BaseImageTag), - Runtimes: dockerbuild.HostPath(env.EncoreRuntimesPath()), + Meta: parse.Meta, + Compile: result, + BundleSource: option.Option[dockerbuild.BundleSourceSpec]{}, + DockerBaseImage: option.AsOptional(params.BaseImageTag), + Runtimes: dockerbuild.HostPath(env.EncoreRuntimesPath()), + NodeRuntime: crossNodeRuntime, + ProcessPerService: app.ProcessPerService(), }) if err != nil { return false, errors.Wrap(err, "describe docker image") @@ -111,20 +123,26 @@ func Docker(ctx context.Context, app *apps.Instance, req *daemonpb.ExportRequest var baseImgOverride option.Option[v1.Image] if params.BaseImageTag != "" { - baseImg, err := resolveBaseImage(ctx, log, params) + baseImg, err := resolveBaseImage(ctx, log, params, spec) if err != nil { return false, errors.Wrap(err, "resolve base image") } baseImgOverride = option.Some(baseImg) } + var supervisorPath option.Option[dockerbuild.HostPath] + if spec.Supervisor.Present() { + binary, err := downloadBinary(req.Goos, req.Goarch, "supervisor-encore", log) + if err != nil { + return false, errors.Wrap(err, "download supervisor binaries") + } + supervisorPath = option.Some(binary) + } img, err := dockerbuild.BuildImage(ctx, spec, dockerbuild.ImageBuildConfig{ BuildTime: time.Now(), BaseImageOverride: baseImgOverride, AddCACerts: option.Some[dockerbuild.ImagePath](""), - - // Not supported yet: - SupervisorPath: option.None[dockerbuild.HostPath](), + SupervisorPath: supervisorPath, }) if err != nil { return false, errors.Wrap(err, "build docker image") @@ -162,7 +180,7 @@ func Docker(ctx context.Context, app *apps.Instance, req *daemonpb.ExportRequest return true, nil } -func resolveBaseImage(ctx context.Context, log zerolog.Logger, p *daemonpb.DockerExportParams) (v1.Image, error) { +func resolveBaseImage(ctx context.Context, log zerolog.Logger, p *daemonpb.DockerExportParams, spec *dockerbuild.ImageSpec) (v1.Image, error) { baseImgTag := p.BaseImageTag if baseImgTag == "" || baseImgTag == "scratch" { return empty.Image, nil @@ -175,15 +193,24 @@ func resolveBaseImage(ctx context.Context, log zerolog.Logger, p *daemonpb.Docke return nil, errors.Wrap(err, "parse base image") } + fetchRemote := true img, err := daemon.Image(baseImgRef) - if err != nil { + if err == nil { + file, err := img.ConfigFile() + if err == nil { + fetchRemote = file.OS != spec.OS || file.Architecture != spec.Arch + } + } + if fetchRemote { log.Info().Msg("could not get image from local daemon, fetching it remotely") keychain := authn.DefaultKeychain - img, err = remote.Image(baseImgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx)) + img, err = remote.Image(baseImgRef, remote.WithAuthFromKeychain(keychain), remote.WithContext(ctx), remote.WithPlatform(v1.Platform{ + OS: spec.OS, + Architecture: spec.Arch, + })) if err != nil { return nil, errors.Wrap(err, "unable to fetch image") } - // If the user requested to push the image locally, save the remote image locally as well. if p.LocalDaemonTag != "" { if tag, err := name.NewTag(baseImgTag, name.WeakValidation); err == nil { diff --git a/pkg/appfile/appfile.go b/pkg/appfile/appfile.go index 81a89c6192..47b47fcee3 100644 --- a/pkg/appfile/appfile.go +++ b/pkg/appfile/appfile.go @@ -83,6 +83,10 @@ type Docker struct { // WorkingDir specifies the working directory to start the docker image in. // If empty it defaults to "/workspace" if the source code is bundled, and to "/" otherwise. WorkingDir string `json:"working_dir,omitempty"` + + // ProcessPerService specifies whether each service should run in its own process. If false, + // all services are run in the same process. + ProcessPerService bool `json:"process_per_service,omitempty"` } type CORS struct { @@ -183,3 +187,12 @@ func GlobalCORS(appRoot string) (*CORS, error) { } return f.GlobalCORS, nil } + +// AppLang returns the language of the app located at appRoot. +func AppLang(appRoot string) (Lang, error) { + f, err := ParseFile(filepath.Join(appRoot, Name)) + if err != nil { + return "", err + } + return f.Lang, nil +} diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 8f694e3556..b0939b0f15 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -64,6 +64,10 @@ type BuildInfo struct { Logger option.Option[zerolog.Logger] } +func (b *BuildInfo) IsCrossBuild() bool { + return b.GOOS != runtime.GOOS || b.GOARCH != runtime.GOARCH +} + // DefaultBuildInfo returns a BuildInfo with default values. // It can be modified afterwards. func DefaultBuildInfo() BuildInfo { @@ -164,6 +168,8 @@ type Cmd struct { } type CompileResult struct { + OS string + Arch string Outputs []BuildOutput } diff --git a/pkg/dockerbuild/dockerbuild.go b/pkg/dockerbuild/dockerbuild.go index 39d2c5c665..add5c791ae 100644 --- a/pkg/dockerbuild/dockerbuild.go +++ b/pkg/dockerbuild/dockerbuild.go @@ -48,7 +48,13 @@ type ImageBuildConfig struct { // BuildImage builds a docker image from the given spec. func BuildImage(ctx context.Context, spec *ImageSpec, cfg ImageBuildConfig) (v1.Image, error) { - baseImg, err := resolveBaseImage(ctx, spec.DockerBaseImage, cfg.BaseImageOverride) + options := []remote.Option{ + remote.WithPlatform(v1.Platform{ + OS: spec.OS, + Architecture: spec.Arch, + }), + } + baseImg, err := resolveBaseImage(ctx, spec.DockerBaseImage, cfg.BaseImageOverride, options...) if err != nil { return nil, errors.Wrap(err, "resolve base image") } @@ -117,7 +123,7 @@ func BuildImage(ctx context.Context, spec *ImageSpec, cfg ImageBuildConfig) (v1. // ResolveRemoteImage resolves the base image with the given reference. // If imageRef is the empty string or "scratch" it resolves to the empty image. -func ResolveRemoteImage(ctx context.Context, imageRef string) (v1.Image, error) { +func ResolveRemoteImage(ctx context.Context, imageRef string, options ...remote.Option) (v1.Image, error) { if imageRef == "" || imageRef == "scratch" { return empty.Image, nil } @@ -128,18 +134,18 @@ func ResolveRemoteImage(ctx context.Context, imageRef string) (v1.Image, error) return nil, errors.Wrap(err, "parse image ref") } - img, err := remote.Image(baseImgRef, remote.WithContext(ctx)) + img, err := remote.Image(baseImgRef, append(options, remote.WithContext(ctx))...) if err != nil { return nil, errors.Wrap(err, "fetch image") } return img, nil } -func resolveBaseImage(ctx context.Context, baseImgTag string, overrideBaseImage option.Option[v1.Image]) (v1.Image, error) { +func resolveBaseImage(ctx context.Context, baseImgTag string, overrideBaseImage option.Option[v1.Image], options ...remote.Option) (v1.Image, error) { if override, ok := overrideBaseImage.Get(); ok { return override, nil } - return ResolveRemoteImage(ctx, baseImgTag) + return ResolveRemoteImage(ctx, baseImgTag, options...) } func buildImageFilesystem(ctx context.Context, spec *ImageSpec, cfg *ImageBuildConfig) (opener tarball.Opener, err error) { diff --git a/pkg/dockerbuild/spec.go b/pkg/dockerbuild/spec.go index b3d3e463a0..abcbf775e6 100644 --- a/pkg/dockerbuild/spec.go +++ b/pkg/dockerbuild/spec.go @@ -26,6 +26,12 @@ type ImageSpecFile struct { // ImageSpec is a specification for how to build a docker image. type ImageSpec struct { + // The operating system to use for the image. + OS string + + // The architecture to use for the image. + Arch string + // The entrypoint to use for the image. It must be non-empty. // The first entry is the executable path, and the rest are the arguments. Entrypoint []string @@ -122,6 +128,9 @@ type DescribeConfig struct { // The directory containing the runtimes. Runtimes HostPath + // The path to the node runtime, if any. + NodeRuntime option.Option[HostPath] + // The docker base image to use, if any. If None it defaults to the empty scratch image. DockerBaseImage option.Option[string] @@ -134,6 +143,9 @@ type DescribeConfig struct { // BuildInfo contains information about the build. BuildInfo BuildInfo + + // ProcessPerService specifies whether to run each service in a separate process. + ProcessPerService bool } type ( @@ -231,37 +243,20 @@ func (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) { } // Determine if we should use the supervisor. - // We should use it in all cases, except where we have a single Go entrypoint. - useSupervisor := true - if len(cfg.Compile.Outputs) == 1 && len(cfg.Compile.Outputs[0].GetEntrypoints()) == 1 { - ep := cfg.Compile.Outputs[0].GetEntrypoints()[0] - if out, ok := cfg.Compile.Outputs[0].(*builder.GoBuildOutput); ok { - imageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())] - if !ok { - return nil, errors.Errorf("missing image artifact dir for %q", out.GetArtifactDir()) - } + // We must use the supervisor if we have more than one service or gateway. + useSupervisor := cfg.ProcessPerService || len(cfg.Compile.Outputs) > 1 || len(cfg.Compile.Outputs[0].GetEntrypoints()) > 1 - cmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts)) - b.spec.Entrypoint = cmd.Command - b.spec.Env = cmd.Env - useSupervisor = false - } else if out, ok := cfg.Compile.Outputs[0].(*builder.JSBuildOutput); ok { - imageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())] - if !ok { - return nil, errors.Errorf("missing image artifact dir for %q", out.GetArtifactDir()) - } - - cmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts)) - b.spec.Entrypoint = cmd.Command - b.spec.Env = cmd.Env - - useSupervisor = false - // If we have a supervisor, we need to use the new runtime config. - b.spec.FeatureFlags[NewRuntimeConfig] = true + if !useSupervisor { + ep := cfg.Compile.Outputs[0].GetEntrypoints()[0] + out := cfg.Compile.Outputs[0] + imageArtifacts, ok := b.seenArtifactDirs[HostPath(out.GetArtifactDir())] + if !ok { + return nil, errors.Errorf("missing image artifact dir for %q", out.GetArtifactDir()) } - } - - if useSupervisor { + cmd := ep.Cmd.Expand(paths.FS(imageArtifacts.BuildArtifacts)) + b.spec.Entrypoint = cmd.Command + b.spec.Env = cmd.Env + } else { config := &supervisor.Config{ NoopGateways: make(map[string]*noopgateway.Description), } @@ -313,8 +308,10 @@ func (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) { b.spec.Supervisor = option.Some(super) b.spec.Entrypoint = []string{string(super.MountPath), "-c", string(super.ConfigPath)} b.spec.Env = nil // not needed by supervisor + } - // If we have a supervisor, we need to use the new runtime config. + // TS apps use runtime config v2. + if cfg.Meta.Language == meta.Lang_TYPESCRIPT { b.spec.FeatureFlags[NewRuntimeConfig] = true } @@ -380,7 +377,7 @@ func (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) { b.spec.CopyData[runtimeSrc.ToImage()] = runtimeSrc // Add the encore-runtime.node file, and set the environment variable to point to it. - nativeRuntimeHost := cfg.Runtimes.Join("js", "encore-runtime.node") + nativeRuntimeHost := cfg.NodeRuntime.GetOrElse(cfg.Runtimes.Join("js", "encore-runtime.node")) nativeRuntimeImg := nativeRuntimeHost.ToImage() b.spec.CopyData[nativeRuntimeImg] = nativeRuntimeHost b.spec.Env = append(b.spec.Env, fmt.Sprintf("ENCORE_RUNTIME_LIB=%s", nativeRuntimeImg)) @@ -391,9 +388,10 @@ func (b *imageSpecBuilder) Describe(cfg DescribeConfig) (*ImageSpec, error) { } b.spec.DockerBaseImage = cfg.DockerBaseImage.GetOrElse("scratch") - b.spec.BundleSource = cfg.BundleSource b.spec.WorkingDir = cfg.WorkingDir.GetOrElse("/") + b.spec.OS = cfg.Compile.OS + b.spec.Arch = cfg.Compile.Arch // Include build information. b.spec.BuildInfo = BuildInfoSpec{ diff --git a/pkg/dockerbuild/spec_test.go b/pkg/dockerbuild/spec_test.go index 3d8e4d2a44..67b8976bef 100644 --- a/pkg/dockerbuild/spec_test.go +++ b/pkg/dockerbuild/spec_test.go @@ -17,7 +17,7 @@ import ( func TestBuild_Node(t *testing.T) { c := qt.New(t) cfg := DescribeConfig{ - Meta: &meta.Data{}, + Meta: &meta.Data{Language: meta.Lang_TYPESCRIPT}, Runtimes: "/host/runtimes", Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.JSBuildOutput{ @@ -113,7 +113,7 @@ func TestBuild_Go_SingleBinary(t *testing.T) { func TestBuild_Go_MultiProc(t *testing.T) { c := qt.New(t) cfg := DescribeConfig{ - Meta: &meta.Data{}, + Meta: &meta.Data{Language: meta.Lang_TYPESCRIPT}, Compile: &builder.CompileResult{Outputs: []builder.BuildOutput{ &builder.GoBuildOutput{ ArtifactDir: "/host/artifacts", diff --git a/pkg/encorebuild/buildconf/config.go b/pkg/encorebuild/buildconf/config.go index 544f6a7159..e18c6058c6 100644 --- a/pkg/encorebuild/buildconf/config.go +++ b/pkg/encorebuild/buildconf/config.go @@ -36,7 +36,7 @@ type Config struct { PublishNPMPackages bool // Whether to copy the built native module back to the repo dir. - CopyNativeModuleToRepo bool + CopyToRepo bool } // IsCross reports whether the build is a cross-compile. diff --git a/pkg/encorebuild/cmd/build-local-binary/build-local-binary.go b/pkg/encorebuild/cmd/build-local-binary/build-local-binary.go new file mode 100644 index 0000000000..55182f8f4f --- /dev/null +++ b/pkg/encorebuild/cmd/build-local-binary/build-local-binary.go @@ -0,0 +1,70 @@ +package main + +import ( + "flag" + "os" + "path/filepath" + "runtime" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + + "encr.dev/internal/version" + "encr.dev/pkg/encorebuild" + "encr.dev/pkg/encorebuild/buildconf" + "encr.dev/pkg/option" +) + +func join(strs ...string) string { + return filepath.Join(strs...) +} + +var archFlag = flag.String("arch", runtime.GOARCH, "the architecture to target") +var osFlag = flag.String("os", runtime.GOOS, "the operating system to target") + +func main() { + binary := os.Args[1] + if binary == "" || binary[0] == '-' { + log.Fatal().Msg("expected binary name as first argument") + } + os.Args = os.Args[1:] + flag.Parse() + log.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger() + + root, err := os.Getwd() + if err != nil { + log.Fatal().Err(err).Msg("failed to get working directory") + } else if _, err := os.Stat(join(root, ".git")); err != nil { + log.Fatal().Err(err).Msg("expected to run build-local-binary from encr.dev repository root") + } + + userCacheDir, err := os.UserCacheDir() + if err != nil { + log.Fatal().Err(err).Msg("failed to get user cache dir") + } + cacheDir := filepath.Join(userCacheDir, "encore-build-cache") + + cfg := &buildconf.Config{ + Log: log.Logger, + OS: *osFlag, + Arch: *archFlag, + Release: false, + Version: version.Version, + RepoDir: root, + CacheDir: cacheDir, + MacSDKPath: option.None[string](), + CopyToRepo: true, + } + + switch binary { + case "all": + encorebuild.NewJSRuntimeBuilder(cfg).Build() + encorebuild.NewSupervisorBuilder(cfg).Build() + case "supervisor-encore": + encorebuild.NewSupervisorBuilder(cfg).Build() + case "encore-runtime.node": + encorebuild.NewJSRuntimeBuilder(cfg).Build() + default: + log.Fatal().Msgf("unknown binary %s", binary) + } +} diff --git a/pkg/encorebuild/cmd/build-local-js-runtime/build-local-js-runtime.go b/pkg/encorebuild/cmd/build-local-js-runtime/build-local-js-runtime.go deleted file mode 100644 index b5cc66bb6c..0000000000 --- a/pkg/encorebuild/cmd/build-local-js-runtime/build-local-js-runtime.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "os" - "path/filepath" - "runtime" - - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" - - "encr.dev/internal/version" - "encr.dev/pkg/encorebuild" - "encr.dev/pkg/encorebuild/buildconf" - "encr.dev/pkg/option" -) - -func join(strs ...string) string { - return filepath.Join(strs...) -} - -func main() { - log.Logger = zerolog.New(zerolog.NewConsoleWriter()).With().Caller().Timestamp().Stack().Logger() - - root, err := os.Getwd() - if err != nil { - log.Fatal().Err(err).Msg("failed to get working directory") - } else if _, err := os.Stat(join(root, ".git")); err != nil { - log.Fatal().Err(err).Msg("expected to run build-local-js-runtime from encr.dev repository root") - } - - userCacheDir, err := os.UserCacheDir() - if err != nil { - log.Fatal().Err(err).Msg("failed to get user cache dir") - } - cacheDir := filepath.Join(userCacheDir, "encore-build-cache") - - cfg := &buildconf.Config{ - Log: log.Logger, - OS: runtime.GOOS, - Arch: runtime.GOARCH, - Release: false, - Version: version.Version, - RepoDir: root, - CacheDir: cacheDir, - MacSDKPath: option.None[string](), - CopyNativeModuleToRepo: true, - } - - builder := encorebuild.NewJSRuntimeBuilder(cfg) - builder.Build() -} diff --git a/pkg/encorebuild/compile/compile.go b/pkg/encorebuild/compile/compile.go index a6d1437b14..4520a33129 100644 --- a/pkg/encorebuild/compile/compile.go +++ b/pkg/encorebuild/compile/compile.go @@ -4,6 +4,7 @@ import ( osPkg "os" "os/exec" "path/filepath" + "runtime" "strings" "sync" @@ -78,7 +79,16 @@ func RustBinary(cfg *buildconf.Config, artifactPath, outputPath string, cratePat } envs := append(extraEnvVars, osPkg.Environ()...) - useZig := cfg.IsCross() || cfg.Release + useCross := false + if cfg.IsCross() && runtime.GOOS == "darwin" { + // check is cross is installed + _, err := exec.LookPath("cross") + if err == nil { + useCross = true + } + } + + useZig := !useCross var target, zigTargetSuffix string switch cfg.OS { @@ -144,7 +154,14 @@ func RustBinary(cfg *buildconf.Config, artifactPath, outputPath string, cratePat buildMode = "release" } - cmd := exec.Command("cargo", cargoArgs...) + builder := "cargo" + if useCross { + builder = "cross" + } + cmd := exec.Command(builder, cargoArgs...) + // forwards the output to the parent process + cmd.Stdout = osPkg.Stdout + cmd.Stderr = osPkg.Stderr cmd.Dir = cratePath cmd.Env = envs @@ -154,8 +171,8 @@ func RustBinary(cfg *buildconf.Config, artifactPath, outputPath string, cratePat defer cargoLock.Unlock() // nosemgrep - if out, err := cmd.CombinedOutput(); err != nil { - Bailf("failed to compile rust binary: %v: %s", err, string(out)) + if err := cmd.Run(); err != nil { + Bailf("failed to compile rust binary: %v", err) } // Copy the binary to the output path diff --git a/pkg/encorebuild/jsruntimebuild.go b/pkg/encorebuild/jsruntimebuild.go index a35f60a23c..aea3505515 100644 --- a/pkg/encorebuild/jsruntimebuild.go +++ b/pkg/encorebuild/jsruntimebuild.go @@ -4,6 +4,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "github.com/rs/zerolog" @@ -37,11 +38,12 @@ type JSRuntimeBuilder struct { } func (b *JSRuntimeBuilder) Build() { + b.log.Info().Msgf("Building local JS runtime targeting %s/%s", b.cfg.OS, b.cfg.Arch) b.buildRustModule() b.genTypeDefWrappers() b.makeDistFolder() - if b.cfg.CopyNativeModuleToRepo { + if b.cfg.CopyToRepo { b.copyNativeModule() } } @@ -73,6 +75,7 @@ func (b *JSRuntimeBuilder) buildRustModule() { "TYPE_DEF_TMP_PATH="+b.typeDefPath(), "ENCORE_VERSION="+b.cfg.Version, + "ENCORE_WORKDIR="+b.workdir, ) } @@ -156,7 +159,11 @@ func (b *JSRuntimeBuilder) copyNativeModule() { } src := b.NativeModuleOutput() - dst := filepath.Join(b.jsRuntimePath(), "encore-runtime.node") + suffix := "" + if b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH { + suffix = "-" + b.cfg.OS + "-" + b.cfg.Arch + } + dst := filepath.Join(b.jsRuntimePath(), "encore-runtime.node"+suffix) copyFile(src, dst) } diff --git a/pkg/encorebuild/supervisorbuild.go b/pkg/encorebuild/supervisorbuild.go new file mode 100644 index 0000000000..47fb6ca4b9 --- /dev/null +++ b/pkg/encorebuild/supervisorbuild.go @@ -0,0 +1,80 @@ +package encorebuild + +import ( + "os" + "os/exec" + "path/filepath" + "runtime" + + "github.com/rs/zerolog" + + "encr.dev/pkg/encorebuild/buildconf" + . "encr.dev/pkg/encorebuild/buildutil" + "encr.dev/pkg/encorebuild/compile" +) + +func NewSupervisorBuilder(cfg *buildconf.Config) *SupervisorBuilder { + if cfg.RepoDir == "" { + Bailf("repo dir not set") + } else if _, err := os.Stat(cfg.RepoDir); err != nil { + Bailf("repo does not exist") + } + + workdir := filepath.Join(cfg.CacheDir, "supervisorbuild", cfg.OS, cfg.Arch) + Check(os.MkdirAll(workdir, 0755)) + return &SupervisorBuilder{ + log: cfg.Log, + cfg: cfg, + workdir: workdir, + } +} + +type SupervisorBuilder struct { + log zerolog.Logger + cfg *buildconf.Config + workdir string +} + +func (b *SupervisorBuilder) Build() { + b.log.Info().Msgf("Building local Supervisor targeting %s/%s", b.cfg.OS, b.cfg.Arch) + b.buildRustModule() + if b.cfg.CopyToRepo { + b.copyToRepo() + } +} + +// buildRustModule builds the Rust module for the Supervisor runtime. +func (b *SupervisorBuilder) buildRustModule() { + b.log.Info().Msg("building rust module") + compile.RustBinary( + b.cfg, + "supervisor-encore", + b.BinaryOutput(), + filepath.Join(b.cfg.RepoDir, "supervisor"), + "ENCORE_VERSION="+b.cfg.Version, + "ENCORE_WORKDIR="+b.workdir, + ) +} + +func (b *SupervisorBuilder) copyToRepo() { + b.log.Info().Msg("copying binary to repo dir") + copyFile := func(src, dst string) { + cmd := exec.Command("cp", src, dst) + out, err := cmd.CombinedOutput() + if err != nil { + Bailf("unable to copy binary: %v: %s", err, out) + } + } + + src := b.BinaryOutput() + suffix := "" + if b.cfg.OS != runtime.GOOS || b.cfg.Arch != runtime.GOARCH { + suffix = "-" + b.cfg.OS + "-" + b.cfg.Arch + } + dst := filepath.Join(b.cfg.RepoDir, "runtimes", "supervisor-encore"+suffix) + copyFile(src, dst) +} + +func (b *SupervisorBuilder) BinaryOutput() string { + return filepath.Join(b.workdir, "supervisor-encore") +} diff --git a/runtimes/core/src/lib.rs b/runtimes/core/src/lib.rs index 8f73ea7cbd..594b10b704 100644 --- a/runtimes/core/src/lib.rs +++ b/runtimes/core/src/lib.rs @@ -1,5 +1,5 @@ use std::borrow::Borrow; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::hash::Hash; use std::io::Read; @@ -11,6 +11,7 @@ use anyhow::Context; use base64::Engine; use duct::cmd; use prost::Message; +use serde::{Deserialize, Serialize}; use crate::api::reqauth::platform; pub use names::{CloudName, EncoreName, EndpointName}; @@ -52,8 +53,64 @@ pub mod encore { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProcessConfig { + hosted_services: Vec, + hosted_gateways: Vec, + local_service_ports: HashMap, +} + +impl ProcessConfig { + pub fn apply(&self, cfg: &mut runtimepb::RuntimeConfig) { + let deployment = cfg.deployment.get_or_insert_with(Default::default); + + deployment.hosted_services = self + .hosted_services + .iter() + .map(|s| runtimepb::HostedService { + name: s.clone(), + ..Default::default() + }) + .collect(); + deployment.hosted_gateways = self + .hosted_gateways + .iter() + .map(|s| { + cfg.infra + .as_ref() + .expect("infra not found in runtime config") + .resources + .as_ref() + .expect("resources not found in infra") + .gateways + .iter() + .find(|g| g.encore_name == *s) + .expect("gateway rid not found in infra resources") + .rid + .clone() + }) + .collect(); + + let svc_discovery = deployment + .service_discovery + .get_or_insert_with(Default::default); + // Iterate through service_ports and add service_discovery entries + for (service_name, port) in &self.local_service_ports { + let base_url = format!("http://127.0.0.1:{}", port); + svc_discovery.services.insert( + service_name.clone(), + runtimepb::service_discovery::Location { + base_url: base_url.clone(), + auth_methods: deployment.auth_methods.clone(), + }, + ); + } + } +} + pub struct RuntimeBuilder { cfg: Option, + proc_cfg: Option, md: Option, err: Option, test_mode: bool, @@ -70,6 +127,7 @@ impl RuntimeBuilder { pub fn new() -> Self { Self { cfg: None, + proc_cfg: None, md: None, err: None, test_mode: false, @@ -95,6 +153,11 @@ impl RuntimeBuilder { self } + pub fn with_proc_config(mut self, proc_cfg: ProcessConfig) -> Self { + self.proc_cfg = Some(proc_cfg); + self + } + pub fn with_runtime_config_from_env(mut self) -> Self { if self.err.is_none() { match runtime_config_from_env() { @@ -103,6 +166,12 @@ impl RuntimeBuilder { self.err = Some(anyhow::Error::new(e).context("unable to parse runtime config")) } } + match proc_config_from_env() { + Ok(cfg) => self.proc_cfg = cfg, + Err(e) => { + self.err = Some(anyhow::Error::new(e).context("unable to parse process config")) + } + } } self } @@ -164,9 +233,11 @@ impl RuntimeBuilder { if let Some(err) = self.err { return Err(err); } - let cfg = self.cfg.context("runtime config not provided")?; + let mut cfg = self.cfg.context("runtime config not provided")?; let md = self.md.context("metadata not provided")?; - + if let Some(proc_config) = self.proc_cfg { + proc_config.apply(&mut cfg); + } Runtime::new(cfg, md, self.test_mode, self.is_worker) } } @@ -409,6 +480,26 @@ fn runtime_config_from_env() -> Result { } } +fn proc_config_from_env() -> Result, ParseError> { + let encoded_config = match std::env::var("ENCORE_PROCESS_CONFIG") { + Ok(config) => config, + Err(std::env::VarError::NotPresent) => return Ok(None), + Err(e) => return Err(ParseError::EnvVar(e)), + }; + + let decoded = base64::engine::general_purpose::STANDARD + .decode(encoded_config) + .map_err(ParseError::Base64)?; + + let json_str = String::from_utf8(decoded) + .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; + + let config = serde_json::from_str(&json_str) + .map_err(|e| ParseError::IO(std::io::Error::new(std::io::ErrorKind::InvalidData, e)))?; + + Ok(Some(config)) +} + fn meta_from_env() -> Result { let cfg = match std::env::var("ENCORE_APP_META") { Ok(cfg) => cfg, diff --git a/runtimes/go/appruntime/exported/config/parse.go b/runtimes/go/appruntime/exported/config/parse.go index 39eab54b39..041baa81e2 100644 --- a/runtimes/go/appruntime/exported/config/parse.go +++ b/runtimes/go/appruntime/exported/config/parse.go @@ -5,9 +5,11 @@ import ( "compress/gzip" "encoding/base64" "encoding/json" + "fmt" "io" "log" "net/url" + "slices" "strings" ) @@ -19,8 +21,14 @@ func gunzip(data []byte) ([]byte, error) { return io.ReadAll(gz) } +type ProcessConfig struct { + HostedServices []string `json:"hosted_services"` + HostedGateways []string `json:"hosted_gateways"` + LocalServicePorts map[string]int `json:"local_service_ports"` +} + // ParseRuntime parses the Encore runtime config. -func ParseRuntime(config, deployID string) *Runtime { +func ParseRuntime(config, processCfg, deployID string) *Runtime { if config == "" { log.Fatalln("encore runtime: fatal error: no encore runtime config provided") } @@ -53,6 +61,45 @@ func ParseRuntime(config, deployID string) *Runtime { log.Fatalln("encore runtime: fatal error: could not parse api base url from encore runtime config:", err) } + if processCfg != "" { + if bytes, err = base64.StdEncoding.DecodeString(processCfg); err != nil { + log.Fatalln("encore runtime: fatal error: could not decode encore process config:", err) + } + var procCfg ProcessConfig + if err := json.Unmarshal(bytes, &procCfg); err != nil { + log.Fatalln("encore runtime: fatal error: could not parse encore process config:", err) + } + cfg.HostedServices = procCfg.HostedServices + var hostedGateways []Gateway + for _, name := range procCfg.HostedGateways { + i := slices.IndexFunc(cfg.Gateways, func(gw Gateway) bool { return gw.Name == name }) + if i == -1 { + log.Fatalf("encore runtime: fatal error: gateway %q not found in runtime config", name) + } + hostedGateways = append(hostedGateways, cfg.Gateways[i]) + } + cfg.Gateways = hostedGateways + + // Use noop service auth method if not specified + svcAuth := ServiceAuth{"noop"} + if len(cfg.ServiceAuth) > 0 { + // Use the first service auth method from the runtime config + svcAuth = cfg.ServiceAuth[0] + } + + for name, port := range procCfg.LocalServicePorts { + if cfg.ServiceDiscovery == nil { + cfg.ServiceDiscovery = make(map[string]Service) + } + cfg.ServiceDiscovery[name] = Service{ + Name: name, + URL: fmt.Sprintf("http://localhost:%d", port), + Protocol: Http, + ServiceAuth: svcAuth, + } + } + } + // If the environment deploy ID is set, use that instead of the one // embedded in the runtime config if deployID != "" { diff --git a/runtimes/go/appruntime/exported/config/parse_test.go b/runtimes/go/appruntime/exported/config/parse_test.go index c64bdfd20a..a8b6b32d23 100644 --- a/runtimes/go/appruntime/exported/config/parse_test.go +++ b/runtimes/go/appruntime/exported/config/parse_test.go @@ -23,8 +23,10 @@ func gzipData(data []byte) ([]byte, error) { func TestGZippedContent(t *testing.T) { var tests = map[string]struct { - GZip bool - Config *Runtime + GZip bool + Config *Runtime + ProcessConfig *ProcessConfig + MergedConfig *Runtime }{ "zipped": { GZip: true, @@ -51,6 +53,44 @@ func TestGZippedContent(t *testing.T) { }, }, }, + "process-config-wo-gw": { + GZip: false, + Config: &Runtime{ + AppSlug: "test", + HostedServices: []string{"one", "two", "three"}, + Gateways: []Gateway{{ + Name: "test", + }}, + }, + ProcessConfig: &ProcessConfig{ + HostedServices: []string{"one"}, + }, + MergedConfig: &Runtime{ + AppSlug: "test", + HostedServices: []string{"one"}, + }, + }, + "process-config-w-gw": { + GZip: false, + Config: &Runtime{ + AppSlug: "test", + HostedServices: []string{"one", "two", "three"}, + Gateways: []Gateway{{ + Name: "test", + Host: "test", + }}, + }, + ProcessConfig: &ProcessConfig{ + HostedGateways: []string{"test"}, + }, + MergedConfig: &Runtime{ + AppSlug: "test", + Gateways: []Gateway{{ + Name: "test", + Host: "test", + }}, + }, + }, } for name, test := range tests { @@ -69,9 +109,20 @@ func TestGZippedContent(t *testing.T) { } else { cfgString = base64.StdEncoding.EncodeToString(rawData) } - resp := ParseRuntime(cfgString, "") - if !reflect.DeepEqual(resp, test.Config) { - t.Fatalf("expected %v, got %v", test.Config, resp) + + expected := test.Config + procCfg := "" + if test.ProcessConfig != nil { + expected = test.MergedConfig + rawData, err := json.Marshal(test.ProcessConfig) + if err != nil { + t.Fatalf("could not marshal process config: %v", err) + } + procCfg = base64.StdEncoding.EncodeToString(rawData) + } + resp := ParseRuntime(cfgString, procCfg, "") + if !reflect.DeepEqual(resp, expected) { + t.Fatalf("expected %+v, got %+v", test.Config, resp) } }) } diff --git a/runtimes/go/appruntime/shared/appconf/appconf.go b/runtimes/go/appruntime/shared/appconf/appconf.go index 4c40f99220..6cd3e84d4e 100644 --- a/runtimes/go/appruntime/shared/appconf/appconf.go +++ b/runtimes/go/appruntime/shared/appconf/appconf.go @@ -28,6 +28,7 @@ func init() { Runtime = config.ParseRuntime( encoreenv.Get("ENCORE_RUNTIME_CONFIG"), + encoreenv.Get("ENCORE_PROCESS_CONFIG"), encoreenv.Get("ENCORE_DEPLOY_ID"), ) } diff --git a/v2/tsbuilder/tsbuilder.go b/v2/tsbuilder/tsbuilder.go index bfda407907..c3191298d3 100644 --- a/v2/tsbuilder/tsbuilder.go +++ b/v2/tsbuilder/tsbuilder.go @@ -226,6 +226,8 @@ func (i *BuilderImpl) Compile(ctx context.Context, p builder.CompileParams) (*bu } return &builder.CompileResult{ + OS: p.Build.GOOS, + Arch: p.Build.GOARCH, Outputs: fns.Map(res.Outputs, func(o *builder.JSBuildOutput) builder.BuildOutput { return o }), diff --git a/v2/v2builder/v2builder.go b/v2/v2builder/v2builder.go index dc1257fa31..422663c14a 100644 --- a/v2/v2builder/v2builder.go +++ b/v2/v2builder/v2builder.go @@ -156,6 +156,8 @@ func (BuilderImpl) Compile(ctx context.Context, p builder.CompileParams) (*build output := &builder.GoBuildOutput{ArtifactDir: buildResult.Dir} res = &builder.CompileResult{ + OS: p.Build.GOOS, + Arch: p.Build.GOARCH, Outputs: []builder.BuildOutput{output}, }