Skip to content
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

APIs to support Terminal integration #638

Merged
merged 11 commits into from
Aug 6, 2024
21 changes: 20 additions & 1 deletion internal/cmd/beta/beta_cmd.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package beta

import (
"fmt"
"io"

"github.com/spf13/cobra"
"github.com/spf13/pflag"

Expand All @@ -10,8 +13,10 @@ import (
)

type commonFlags struct {
silent bool
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved
categories []string
filename string
subtle bool
}

func BetaCmd() *cobra.Command {
Expand All @@ -27,7 +32,11 @@ All commands are experimental and not yet ready for production use.
All commands use the runme.yaml configuration file.`,
Hidden: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
return autoconfig.InvokeForCommand(func(cfg *config.Config) error {
if cFlags.silent {
cmd.SetErr(io.Discard)
}

err := autoconfig.InvokeForCommand(func(cfg *config.Config) error {
// Override the filename if provided.
if cFlags.filename != "" {
cfg.ProjectFilename = cFlags.filename
Expand All @@ -44,6 +53,13 @@ All commands use the runme.yaml configuration file.`,

return nil
})

// print the error to stderr but don't return it
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
_, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s\n", err)
}

return nil
},
}

Expand All @@ -54,6 +70,8 @@ All commands use the runme.yaml configuration file.`,
pFlags := cmd.PersistentFlags()
pFlags.StringVar(&cFlags.filename, "filename", "", "Name of the Markdown file to run blocks from.")
pFlags.StringSliceVar(&cFlags.categories, "category", nil, "Run blocks only from listed categories.")
pFlags.BoolVar(&cFlags.silent, "silent", false, "Silent mode. Do not error messages.")
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved
pFlags.BoolVar(&cFlags.subtle, "subtle", false, "Explicitly allow delicate operations to prevent misuse")
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved

// Hide all persistent flags from the root command.
// "beta" is a completely different set of commands and
Expand All @@ -73,6 +91,7 @@ All commands use the runme.yaml configuration file.`,
cmd.AddCommand(printCmd(cFlags))
cmd.AddCommand(server.Cmd())
cmd.AddCommand(runCmd(cFlags))
cmd.AddCommand(envCmd(cFlags))

return &cmd
}
119 changes: 119 additions & 0 deletions internal/cmd/beta/env_cmd.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package beta

import (
"fmt"
"os"
"strings"

"github.com/pkg/errors"
"github.com/spf13/cobra"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"

runmetls "github.com/stateful/runme/v3/internal/tls"
runnerv2 "github.com/stateful/runme/v3/pkg/api/gen/proto/go/runme/runner/v2alpha1"
)

func envCmd(cflags *commonFlags) *cobra.Command {
cmd := cobra.Command{
Use: "env",
Aliases: []string{"environment"},
Hidden: true,
Short: "Environment management",
Long: "Various commands to manage environments in runme",
}

cmd.AddCommand(envSourceCmd(cflags))

return &cmd
}

func envSourceCmd(cflags *commonFlags) *cobra.Command {
var (
serverAddr string
sessionID string
sessionStrategy string
tlsDir string
fExport bool
)

cmd := cobra.Command{
Use: "source",
Short: "Source environment variables from session",
Long: "Source environment variables from session",
RunE: func(cmd *cobra.Command, args []string) error {
// discard any stderr in silent mode
if !cflags.subtle {
return errors.New("must be run in subtle mode to prevent misuse; enable by adding --subtle flag")
}

tlsConfig, err := runmetls.LoadClientConfigFromDir(tlsDir)
if err != nil {
return err
}

credentials := credentials.NewTLS(tlsConfig)
conn, err := grpc.Dial(serverAddr, grpc.WithTransportCredentials(credentials))
if err != nil {
return errors.Wrap(err, "failed to connect")
}
defer conn.Close()

client := runnerv2.NewRunnerServiceClient(conn)

// todo(sebastian): would it be better to require a specific session?
if strings.ToLower(sessionStrategy) == "recent" {
req := &runnerv2.ListSessionsRequest{}
resp, err := client.ListSessions(cmd.Context(), req)
if err != nil {
return err
}
l := len(resp.Sessions)
if l == 0 {
return errors.New("no sessions found")
}
// potentially unreliable
sessionID = resp.Sessions[l-1].Id
}

req := &runnerv2.GetSessionRequest{Id: sessionID}
resp, err := client.GetSession(cmd.Context(), req)
if err != nil {
return err
}

for _, kv := range resp.Session.Env {
parts := strings.Split(kv, "=")
if len(parts) < 2 {
return errors.Errorf("invalid key-value pair: %s", kv)
}

envVar := fmt.Sprintf("%s=%q", parts[0], strings.Join(parts[1:], "="))
if fExport {
envVar = fmt.Sprintf("export %s", envVar)
}

if _, err := fmt.Fprintf(cmd.OutOrStdout(), "%s\n", envVar); err != nil {
return err
}
}

return nil
},
}

cmd.Flags().StringVar(&serverAddr, "server-address", os.Getenv("RUNME_SERVER_ADDR"), "The Server ServerAddress to connect to, i.e. 127.0.0.1:7865")
cmd.Flags().StringVar(&tlsDir, "tls-dir", os.Getenv("RUNME_TLS_DIR"), "Path to tls files")
cmd.Flags().StringVar(&sessionID, "session", os.Getenv("RUNME_SESSION"), "Session Id")
cmd.Flags().StringVar(&sessionStrategy, "session-strategy", func() string {
if val, ok := os.LookupEnv("RUNME_SESSION_STRATEGY"); ok {
return val
}

return "manual"
}(), "Strategy for session selection. Options are manual, recent. Defaults to manual")

cmd.Flags().BoolVarP(&fExport, "export", "", false, "export variables")

return &cmd
}
6 changes: 3 additions & 3 deletions internal/cmd/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,8 @@ func storeSnapshotCmd() *cobra.Command {
},
}

cmd.Flags().StringVar(&serverAddr, "ServerAddress", os.Getenv("RUNME_SERVER_ADDR"), "The Server ServerAddress to connect to, i.e. 127.0.0.1:7865")
cmd.Flags().StringVar(&tlsDir, "TLSDir", os.Getenv("RUNME_TLS_DIR"), "Path to tls files")
cmd.Flags().StringVar(&serverAddr, "server-address", os.Getenv("RUNME_SERVER_ADDR"), "The Server ServerAddress to connect to, i.e. 127.0.0.1:7865")
cmd.Flags().StringVar(&tlsDir, "tls-dir", os.Getenv("RUNME_TLS_DIR"), "Path to tls files")
cmd.Flags().StringVar(&sessionID, "session", os.Getenv("RUNME_SESSION"), "Session Id")
cmd.Flags().StringVar(&sessionStrategy, "session-strategy", func() string {
if val, ok := os.LookupEnv("RUNME_SESSION_STRATEGY"); ok {
Expand Down Expand Up @@ -204,7 +204,7 @@ func environmentDumpCmd() *cobra.Command {
Long: "Dumps all environment variables to stdout as a list of K=V separated by null terminators",
RunE: func(cmd *cobra.Command, args []string) error {
if !fInsecure {
return errors.New("must be run in insecure mode; enable by running with --insecure flag")
return errors.New("must be run in insecure mode to prevent misuse; enable by adding --insecure flag")
}

producer, err := newOSEnvironReader()
Expand Down
4 changes: 3 additions & 1 deletion internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ var (
fRespectGitignore bool
fSkipRunnerFallback bool
fInsecure bool
fSubtle bool
fLogEnabled bool
fLogFilePath string
fExtensionHandle string
Expand Down Expand Up @@ -84,7 +85,8 @@ func Root() *cobra.Command {

pflags.StringVar(&fChdir, "chdir", getCwd(), "Switch to a different working directory before executing the command")
pflags.StringVar(&fFileName, "filename", "README.md", "Name of the README file")
pflags.BoolVar(&fInsecure, "insecure", false, "Run command in insecure-mode")
pflags.BoolVar(&fInsecure, "insecure", false, "Explicitly allow insecure operations to prevent misuse")
pflags.BoolVar(&fSubtle, "subtle", false, "Explicitly allow delicate operations to prevent misuse")

pflags.StringVar(&fProject, "project", "", "Root project to find runnable tasks")
pflags.BoolVar(&fRespectGitignore, "git-ignore", true, "Whether to respect .gitignore file(s) in project")
Expand Down
16 changes: 14 additions & 2 deletions internal/command/command_terminal.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,21 @@ func (c *terminalCommand) Start(ctx context.Context) (err error) {
c.logger.Info("a terminal command started")

if c.envCollector != nil {
return c.envCollector.SetOnShell(c.stdinWriter)
if err = c.envCollector.SetOnShell(c.stdinWriter); err != nil {
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved
return err
}
}
return nil

if _, err := c.stdinWriter.Write([]byte(" eval $(runme beta env source --silent --subtle --export)\n clear\n")); err != nil {
return err
}

// todo(sebastian): good enough for prototype; it makes more sense to write this message at the TTY-level
initMsg := []byte(" # Runme: This terminal forked your session. " +
"Upon exit exported environment variables will be rolled up into the session.\n\n")
_, err = c.stdinWriter.Write(initMsg)

return err
}

func (c *terminalCommand) Wait() (err error) {
Expand Down
6 changes: 5 additions & 1 deletion internal/command/env_collector_fifo_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,11 @@ type envCollectorFifo struct {
temp *tempDirectory
}

func newEnvCollectorFifo(scanner envScanner, encKey, encNonce []byte) (*envCollectorFifo, error) {
func newEnvCollectorFifo(
scanner envScanner,
encKey,
encNonce []byte,
) (*envCollectorFifo, error) {
temp, err := newTempDirectory()
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion internal/command/env_collector_file.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var _ envCollector = (*envCollectorFile)(nil)

func newEnvCollectorFile(
scanner envScanner,
encKey []byte,
encKey,
encNonce []byte,
) (*envCollectorFile, error) {
temp, err := newTempDirectory()
Expand Down
11 changes: 8 additions & 3 deletions internal/command/env_shell.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,24 @@ import (

func setOnShell(shell io.Writer, prePath, postPath string) error {
var err error

// Prefix commands with a space to avoid polluting the shell history.
sourishkrout marked this conversation as resolved.
Show resolved Hide resolved
skipShellHistory := " "

// First, dump all env at the beginning, so that a diff can be calculated.
_, err = shell.Write([]byte(envDumpCommand + " > " + prePath + "\n"))
_, err = shell.Write([]byte(skipShellHistory + envDumpCommand + " > " + prePath + "\n"))
if err != nil {
return err
}
// Then, set a trap on EXIT to dump all env at the end.
_, err = shell.Write(bytes.Join(
[][]byte{
[]byte("__cleanup() {\nrv=$?\n" + (envDumpCommand + " > " + postPath) + "\nexit $rv\n}"),
[]byte("trap -- \"__cleanup\" EXIT"),
[]byte(skipShellHistory + "__cleanup() {\nrv=$?\n" + (envDumpCommand + " > " + postPath) + "\nexit $rv\n}"),
[]byte(skipShellHistory + "trap -- \"__cleanup\" EXIT"),
nil, // add a new line at the end
},
[]byte{'\n'},
))

return err
}
28 changes: 28 additions & 0 deletions internal/command/env_shell_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//go:build !windows
// +build !windows

package command

import (
"bytes"
"testing"

"github.com/stretchr/testify/require"
)

func TestSetOnShell(t *testing.T) {
t.Parallel()

buf := new(bytes.Buffer)

err := setOnShell(buf, "prePath", "postPath")
require.NoError(t, err)

expected := " " +
envDumpCommand +
" > prePath\n __cleanup() {\nrv=$?\n" +
envDumpCommand +
" > postPath\nexit $rv\n}\n trap -- \"__cleanup\" EXIT\n"

require.EqualValues(t, expected, buf.String())
}
3 changes: 3 additions & 0 deletions internal/runnerv2service/service_sessions.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package runnerv2service
import (
"context"

"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"

Expand Down Expand Up @@ -45,6 +46,8 @@ func (r *runnerService) CreateSession(ctx context.Context, req *runnerv2alpha1.C

r.sessions.Add(sess)

r.logger.Debug("created session", zap.String("id", sess.ID))

return &runnerv2alpha1.CreateSessionResponse{
Session: convertSessionToRunnerv2alpha1Session(sess),
}, nil
Expand Down
Loading