8000 add an exec command for running scripts with inited js runtime by fredr · Pull Request #1822 · encoredev/encore · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

add an exec command for running scripts with inited js runtime #1822

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Apr 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions cli/cmd/encore/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"context"
"os"
"os/signal"
"path/filepath"

"github.com/spf13/cobra"

Expand Down Expand Up @@ -35,18 +34,14 @@ func execScript(appRoot, relWD string, args []string) {
cancel()
}()

commandRelPath := filepath.ToSlash(filepath.Join(relWD, args[0]))
scriptArgs := args[1:]

daemon := setupDaemon(ctx)
stream, err := daemon.ExecScript(ctx, &daemonpb.ExecScriptRequest{
AppRoot: appRoot,
WorkingDir: relWD,
CommandRelPath: commandRelPath,
ScriptArgs: scriptArgs,
Environ: os.Environ(),
TraceFile: root.TraceFile,
Namespace: nonZeroPtr(nsName),
AppRoot: appRoot,
WorkingDir: relWD,
ScriptArgs: args,
Environ: os.Environ(),
TraceFile: root.TraceFile,
Namespace: nonZeroPtr(nsName),
})
if err != nil {
fatal(err)
Expand All @@ -70,4 +65,5 @@ func init() {
func init() {
execCmd.Flags().StringVarP(&nsName, "namespace", "n", "", "Namespace to use (defaults to active namespace)")
alphaCmd.AddCommand(execCmd)
rootCmd.AddCommand(execCmd)
}
83 changes: 54 additions & 29 deletions cli/daemon/exec_script.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"encr.dev/cli/daemon/run"
"encr.dev/internal/optracker"
"encr.dev/pkg/appfile"
"encr.dev/pkg/paths"
daemonpb "encr.dev/proto/encore/daemon"
)
Expand Down Expand Up @@ -46,26 +47,12 @@ func (s *Server) ExecScript(req *daemonpb.ExecScriptRequest, stream daemonpb.Dae
return nil
}

modPath := filepath.Join(app.Root(), "go.mod")
modData, err := os.ReadFile(modPath)
if err != nil {
sendErr(err)
return nil
}
mod, err := modfile.Parse(modPath, modData, nil)
if err != nil {
sendErr(err)
return nil
}

ns, err := s.namespaceOrActive(ctx, app, req.Namespace)
if err != nil {
sendErr(err)
return nil
}

commandPkg := paths.Pkg(mod.Module.Mod.Path).JoinSlash(paths.RelSlash(req.CommandRelPath))

ops := optracker.New(stderr, stream)
defer ops.AllDone() // Kill the tracker when we exit this function

Expand All @@ -84,21 +71,59 @@ func (s *Server) ExecScript(req *daemonpb.ExecScriptRequest, stream daemonpb.Dae
}
}()

p := run.ExecScriptParams{
App: app,
NS: ns,
WorkingDir: req.WorkingDir,
Environ: req.Environ,
MainPkg: commandPkg,
ScriptArgs: req.ScriptArgs,
Stdout: slog.Stdout(false),
Stderr: slog.Stderr(false),
OpTracker: ops,
}
if err := s.mgr.ExecScript(stream.Context(), p); err != nil {
sendErr(err)
} else {
streamExit(stream, 0)
switch app.Lang() {
case appfile.LangGo:
modPath := filepath.Join(app.Root(), "go.mod")
modData, err := os.ReadFile(modPath)
if err != nil {
sendErr(err)
return nil
}
mod, err := modfile.Parse(modPath, modData, nil)
if err != nil {
sendErr(err)
return nil
}

commandRelPath := filepath.ToSlash(filepath.Join(req.WorkingDir, req.ScriptArgs[0]))
scriptArgs := req.ScriptArgs[1:]
commandPkg := paths.Pkg(mod.Module.Mod.Path).JoinSlash(paths.RelSlash(commandRelPath))

p := run.ExecScriptParams{
App: app,
NS: ns,
WorkingDir: req.WorkingDir,
Environ: req.Environ,
MainPkg: commandPkg,
ScriptArgs: scriptArgs,
Stdout: slog.Stdout(false),
Stderr: slog.Stderr(false),
OpTracker: ops,
}
if err := s.mgr.ExecScript(stream.Context(), p); err != nil {
sendErr(err)
} else {
streamExit(stream, 0)
}
case appfile.LangTS:
p := run.ExecCommandParams{
App: app,
NS: ns,
WorkingDir: req.WorkingDir,
Environ: req.Environ,
Command: req.ScriptArgs[0],
ScriptArgs: req.ScriptArgs[1:],
Stdout: slog.Stdout(false),
Stderr: slog.Stderr(false),
OpTracker: ops,
}

if err := s.mgr.ExecCommand(stream.Context(), p); err != nil {
sendErr(err)
} else {
streamExit(stream, 0)
}
}

return nil
}
193 changes: 193 additions & 0 deletions cli/daemon/run/exec_command.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
package run

import (
"context"
"fmt"
"io"
"os/exec"
"path/filepath"
"runtime"
"time"

"github.com/cockroachdb/errors"

"encr.dev/cli/daemon/apps"
"encr.dev/cli/daemon/namespace"
"encr.dev/cli/daemon/run/infra"
"encr.dev/internal/optracker"
"encr.dev/internal/version"
"encr.dev/pkg/builder"
"encr.dev/pkg/builder/builderimpl"
"encr.dev/pkg/cueutil"
"encr.dev/pkg/fns"
"encr.dev/pkg/option"
"encr.dev/pkg/promise"
"encr.dev/pkg/vcs"
)

// ExecCommandParams groups the parameters for the ExecCommand method.
type ExecCommandParams struct {
// App is the app to execute the script for.
App *apps.Instance

// NS is the namespace to use.
NS *namespace.Namespace

// Command to execute
Command string

// ScriptArgs are the arguments to pass to the script binary.
ScriptArgs []string

// WorkingDir is the working dir to execute the script from.
// It's relative to the app root.
WorkingDir string

// Environ are the environment variables to set when running the tests,
// in the same format as os.Environ().
Environ []string

// Stdout and Stderr are where "go test" output should be written.
Stdout, Stderr io.Writer

OpTracker *optracker.OpTracker
}

// ExecCommand executes the script.
func (mgr *Manager) ExecCommand(ctx context.Context, p ExecCommandParams) (err error) {
expSet, err := p.App.Experiments(p.Environ)
if err != nil {
return err
}

rm := infra.NewResourceManager(p.App, mgr.ClusterMgr, mgr.ObjectsMgr, mgr.PublicBuckets, p.NS, p.Environ, mgr.DBProxyPort, false)
defer rm.StopAll()

tracker := p.OpTracker
jobs := optracker.NewAsyncBuildJobs(ctx, p.App.PlatformOrLocalID(), tracker)

// Parse the app to figure out what infrastructure is needed.
start := time.Now()
parseOp := tracker.Add("Building Encore application graph", start)
topoOp := tracker.Add("Analyzing service topology", start)

bld := builderimpl.Resolve(p.App.Lang(), expSet)
defer fns.CloseIgnore(bld)
vcsRevision := vcs.GetRevision(p.App.Root())
buildInfo := builder.BuildInfo{
BuildTags: builder.LocalBuildTags,
CgoEnabled: true,
StaticLink: false,
DebugMode: builder.DebugModeDisabled,
Environ: p.Environ,
GOOS: runtime.GOOS,
GOARCH: runtime.GOARCH,
KeepOutput: false,
Revision: vcsRevision.Revision,
UncommittedChanges: vcsRevision.Uncommitted,

// Use the local JS runtime if this is a development build.
UseLocalJSRuntime: version.Channel == version.DevBuild,
}

parse, err := bld.Parse(ctx, builder.ParseParams{
Build: buildInfo,
App: p.App,
Experiments: expSet,
WorkingDir: p.WorkingDir,
ParseTests: false,
})
if err != nil {
// Don't use the error itself in tracker.Fail, as it will lead to duplicate error output.
tracker.Fail(parseOp, errors.New("parse error"))
return err
}
if err := p.App.CacheMetadata(parse.Meta); err != nil {
return errors.Wrap(err, "cache metadata")
}
tracker.Done(parseOp, 500*time.Millisecond)
tracker.Done(topoOp, 300*time.Millisecond)

rm.StartRequiredServices(jobs, parse.Meta)

var secrets map[string]string
if usesSecrets(parse.Meta) {
jobs.Go("Fetching application secrets", true, 150*time.Millisecond, func(ctx context.Context) error {
data, err := mgr.Secret.Load(p.App).Get(ctx, expSet)
if err != nil {
return err
}
secrets = data.Values
return nil
})
}

apiBaseURL := fmt.Sprintf("http://localhost:%d", mgr.RuntimePort)

configProm := promise.New(func() (*builder.ServiceConfigsResult, error) {
return bld.ServiceConfigs(ctx, builder.ServiceConfigsParams{
Parse: parse,
CueMeta: &cueutil.Meta{
APIBaseURL: apiBaseURL,
EnvName: "local",
EnvType: cueutil.EnvType_Development,
CloudType: cueutil.CloudType_Local,
},
})
})

if err := jobs.Wait(); err != nil {
return err
}

gateways := make(map[string]GatewayConfig)
for _, gw := range parse.Meta.Gateways {
gateways[gw.EncoreName] = GatewayConfig{
BaseURL: apiBaseURL,
Hostnames: []string{"localhost"},
}
}

cfg, err := configProm.Get(ctx)
if err != nil {
return err
}

authKey := genAuthKey()
configGen := &RuntimeConfigGenerator{
app: p.App,
infraManager: rm,
md: parse.Meta,
AppID: option.Some(GenID()),
EnvID: option.Some(GenID()),
TraceEndpoint: option.Some(fmt.Sprintf("http://localhost:%d/trace", mgr.RuntimePort)),
AuthKey: authKey,
Gateways: gateways,
DefinedSecrets: secrets,
SvcConfigs: cfg.Configs,
IncludeMetaEnv: bld.NeedsMeta(),
}
procConf, err := configGen.AllInOneProc()
if err != nil {
return err
}
procEnv, err := configGen.ProcEnvs(procConf, bld.UseNewRuntimeConfig())
if err != nil {
return errors.Wrap(err, "compute proc envs")
}

defaultEnv := []string{"ENCORE_RUNTIME_LOG=error"}
env := append(defaultEnv, p.Environ...)
env = append(env, procConf.ExtraEnv...)
env = append(env, procEnv...)

tracker.AllDone()

// nosemgrep: go.lang.security.audit.dangerous-exec-command.dangerous-exec-command
cmd := exec.CommandContext(ctx, p.Command, p.ScriptArgs...)
cmd.Dir = filepath.Join(p.App.Root(), p.WorkingDir)
cmd.Stdout = p.Stdout
cmd.Stderr = p.Stderr
cmd.Env = env
return cmd.Run()
}
Loading
0