From 284aba024b1b558590e8e2f97dbc3553a5e0c76b Mon Sep 17 00:00:00 2001 From: Jean-Hadrien Chabran Date: Wed, 4 May 2022 09:12:30 +0200 Subject: [PATCH] sg: create a test command to run e2e tests locally (#34627) --- dev/sg/internal/run/command.go | 57 ++++++++++++++++++++++++++++++++-- dev/sg/internal/run/run.go | 26 +++++++++++----- go.mod | 3 +- go.sum | 6 +++- sg.config.yaml | 25 ++++++++++++++- 5 files changed, 105 insertions(+), 12 deletions(-) diff --git a/dev/sg/internal/run/command.go b/dev/sg/internal/run/command.go index 188efd2870f..488a1c39c3e 100644 --- a/dev/sg/internal/run/command.go +++ b/dev/sg/internal/run/command.go @@ -2,12 +2,16 @@ package run import ( "context" + "fmt" "io" "os/exec" + secretmanager "cloud.google.com/go/secretmanager/apiv1" "golang.org/x/sync/errgroup" + secretmanagerpb "google.golang.org/genproto/googleapis/cloud/secretmanager/v1" "github.com/sourcegraph/sourcegraph/dev/sg/internal/stdout" + "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/sourcegraph/lib/output" "github.com/sourcegraph/sourcegraph/lib/process" ) @@ -23,11 +27,20 @@ type Command struct { IgnoreStderr bool `yaml:"ignoreStderr"` DefaultArgs string `yaml:"defaultArgs"` ContinueWatchOnExit bool `yaml:"continueWatchOnExit"` + Secrets map[string]Secret `yaml:"secrets"` + // Preamble is a short and visible message, displayed when the command is launched. + Preamble string `yaml:"preamble"` // ATTENTION: If you add a new field here, be sure to also handle that // field in `Merge` (below). } +type Secret struct { + Provider string `yaml:"provider"` + Project string `yaml:"project"` + Name string `yaml:"name"` +} + func (c Command) Merge(other Command) Command { merged := c @@ -49,12 +62,19 @@ func (c Command) Merge(other Command) Command { if other.DefaultArgs != merged.DefaultArgs && other.DefaultArgs != "" { merged.DefaultArgs = other.DefaultArgs } + if other.Preamble != merged.Preamble && other.Preamble != "" { + merged.Preamble = other.Preamble + } merged.ContinueWatchOnExit = other.ContinueWatchOnExit || merged.ContinueWatchOnExit for k, v := range other.Env { merged.Env[k] = v } + for k, v := range other.Secrets { + merged.Secrets[k] = v + } + if !equal(merged.Watch, other.Watch) && len(other.Watch) != 0 { merged.Watch = other.Watch } @@ -110,7 +130,31 @@ func (sc *startedCmd) CapturedStderr() string { return string(sc.stderrBuf.Bytes()) } -func startCmd(ctx context.Context, dir string, cmd Command, globalEnv map[string]string) (*startedCmd, error) { +func getSecrets(ctx context.Context, cmd Command) (map[string]string, error) { + secretsEnv := map[string]string{} + + client, err := secretmanager.NewClient(ctx) + if err != nil { + return nil, errors.Errorf("failed to create secretmanager client: %v", err) + } + for envName, secret := range cmd.Secrets { + if secret.Provider != "gcloud" { + errors.Newf("Unknown secrets provider %s", secret.Provider) + } + path := fmt.Sprintf("projects/%s/secrets/%s/versions/latest", secret.Project, secret.Name) + req := &secretmanagerpb.AccessSecretVersionRequest{ + Name: path, + } + result, err := client.AccessSecretVersion(ctx, req) + if err != nil { + return nil, errors.Wrapf(err, "failed to access secret %s from %s", secret.Name, secret.Project) + } + secretsEnv[envName] = string(result.Payload.Data) + } + return secretsEnv, nil +} + +func startCmd(ctx context.Context, dir string, cmd Command, parentEnv map[string]string) (*startedCmd, error) { sc := &startedCmd{ stdoutBuf: &prefixSuffixSaver{N: 32 << 10}, stderrBuf: &prefixSuffixSaver{N: 32 << 10}, @@ -121,7 +165,13 @@ func startCmd(ctx context.Context, dir string, cmd Command, globalEnv map[string sc.Cmd = exec.CommandContext(commandCtx, "bash", "-c", cmd.Cmd) sc.Cmd.Dir = dir - sc.Cmd.Env = makeEnv(globalEnv, cmd.Env) + + secretsEnv, err := getSecrets(ctx, cmd) + if err != nil { + return nil, errors.Wrapf(err, "cannot fetch secrets") + } + + sc.Cmd.Env = makeEnv(cmd.Env, secretsEnv, parentEnv) var stdoutWriter, stderrWriter io.Writer logger := newCmdLogger(commandCtx, cmd.Name, stdout.Out) @@ -138,6 +188,9 @@ func startCmd(ctx context.Context, dir string, cmd Command, globalEnv map[string stderrWriter = io.MultiWriter(logger, sc.stderrBuf) } + if cmd.Preamble != "" { + stdout.Out.WriteLine(output.Linef("", output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) + } eg, err := process.PipeOutputUnbuffered(ctx, sc.Cmd, stdoutWriter, stderrWriter) if err != nil { return nil, err diff --git a/dev/sg/internal/run/run.go b/dev/sg/internal/run/run.go index d974dc30340..8f322722b55 100644 --- a/dev/sg/internal/run/run.go +++ b/dev/sg/internal/run/run.go @@ -21,7 +21,7 @@ import ( "github.com/sourcegraph/sourcegraph/lib/output" ) -func Commands(ctx context.Context, globalEnv map[string]string, verbose bool, cmds ...Command) error { +func Commands(ctx context.Context, parentEnv map[string]string, verbose bool, cmds ...Command) error { chs := make([]<-chan struct{}, 0, len(cmds)) monitor := &changeMonitor{} for _, cmd := range cmds { @@ -58,7 +58,7 @@ func Commands(ctx context.Context, globalEnv map[string]string, verbose bool, cm defer wg.Done() var err error for first := true; cmd.ContinueWatchOnExit || first; first = false { - if err = runWatch(ctx, cmd, root, globalEnv, ch, verbose, installed, okayToStart); err != nil { + if err = runWatch(ctx, cmd, root, parentEnv, ch, verbose, installed, okayToStart); err != nil { if errors.Is(err, ctx.Err()) { // if error caused by context, terminate return } @@ -275,7 +275,7 @@ func runWatch( ctx context.Context, cmd Command, root string, - globalEnv map[string]string, + parentEnv map[string]string, reload <-chan struct{}, verbose bool, installDone chan string, @@ -312,7 +312,7 @@ func runWatch( stdout.Out.WriteLine(output.Linef("", output.StylePending, "Installing %s...", cmd.Name)) } - cmdOut, err := BashInRoot(ctx, cmd.Install, makeEnv(globalEnv, cmd.Env)) + cmdOut, err := BashInRoot(ctx, cmd.Install, makeEnv(cmd.Env, parentEnv)) if err != nil { if !startedOnce { return installErr{cmdName: cmd.Name, output: cmdOut, originalErr: err} @@ -363,7 +363,7 @@ func runWatch( // Run it stdout.Out.WriteLine(output.Linef("", output.StylePending, "Running %s...", cmd.Name)) - sc, err := startCmd(ctx, root, cmd, globalEnv) + sc, err := startCmd(ctx, root, cmd, parentEnv) defer sc.cancel() if err != nil { @@ -418,6 +418,9 @@ func runWatch( } } +// makeEnv merges environments starting from the left, meaning the first environment will be overriden by the second one, skipping +// any key that has been explicitly defined in the current environment of this process. This enables users to manually overrides +// environment variables explictly, i.e FOO=1 sg start will have FOO=1 set even if a command or commandset sets FOO. func makeEnv(envs ...map[string]string) []string { combined := os.Environ() expandedEnv := map[string]string{} @@ -561,7 +564,7 @@ func watch() (<-chan string, error) { return paths, nil } -func Test(ctx context.Context, cmd Command, args []string, globalEnv map[string]string) error { +func Test(ctx context.Context, cmd Command, args []string, parentEnv map[string]string) error { root, err := root.RepositoryRoot() if err != nil { return err @@ -581,9 +584,18 @@ func Test(ctx context.Context, cmd Command, args []string, globalEnv map[string] cmdArgs = append(cmdArgs, cmd.DefaultArgs) } + secretsEnv, err := getSecrets(ctx, cmd) + if err != nil { + return errors.Wrapf(err, "cannot fetch secrets") + } + + if cmd.Preamble != "" { + stdout.Out.WriteLine(output.Linef("", output.StyleOrange, "[%s] %s %s", cmd.Name, output.EmojiInfo, cmd.Preamble)) + } + c := exec.CommandContext(commandCtx, "bash", "-c", strings.Join(cmdArgs, " ")) c.Dir = root - c.Env = makeEnv(globalEnv, cmd.Env) + c.Env = makeEnv(parentEnv, secretsEnv, cmd.Env) c.Stdout = os.Stdout c.Stderr = os.Stderr diff --git a/go.mod b/go.mod index bd438563a68..64949980cb9 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( cloud.google.com/go/monitoring v1.2.0 cloud.google.com/go/profiler v0.2.0 cloud.google.com/go/pubsub v1.17.1 + cloud.google.com/go/secretmanager v1.4.0 cloud.google.com/go/storage v1.19.0 github.com/BurntSushi/toml v0.4.1 github.com/Masterminds/semver v1.5.0 @@ -185,7 +186,7 @@ require ( require ( cloud.google.com/go/compute v1.5.0 // indirect - cloud.google.com/go/iam v0.1.1 // indirect + cloud.google.com/go/iam v0.3.0 // indirect github.com/DataDog/datadog-agent/pkg/obfuscate v0.35.1 // indirect github.com/DataDog/datadog-go v4.8.3+incompatible // indirect github.com/DataDog/datadog-go/v5 v5.1.0 // indirect diff --git a/go.sum b/go.sum index a901fd54bcc..e1d3e56e3f7 100644 --- a/go.sum +++ b/go.sum @@ -51,8 +51,9 @@ cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6m cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= -cloud.google.com/go/iam v0.1.1 h1:4CapQyNFjiksks1/x7jsvsygFPhihslYk5GptIrlX68= cloud.google.com/go/iam v0.1.1/go.mod h1:CKqrcnI/suGpybEHxZ7BMehL0oA4LpdyJdUlTl9jVMw= +cloud.google.com/go/iam v0.3.0 h1:exkAomrVUuzx9kWFI1wm3KI0uoDeUFPB4kKGzx6x+Gc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/kms v1.0.0/go.mod h1:nhUehi+w7zht2XrUfvTRNpxrfayBHqP4lu2NSywui/0= cloud.google.com/go/kms v1.1.0 h1:1yc4rLqCkVDS9Zvc7m+3mJ47kw0Uo5Q5+sMjcmUVUeM= cloud.google.com/go/kms v1.1.0/go.mod h1:WdbppnCDMDpOvoYBMn1+gNmOeEoZYqAv+HeuKARGCXI= @@ -67,6 +68,8 @@ cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjp cloud.google.com/go/pubsub v1.4.0/go.mod h1:LFrqilwgdw4X2cJS9ALgzYmMu+ULyrUN6IHV3CPK4TM= cloud.google.com/go/pubsub v1.17.1 h1:s2UGTTphpnUQ0Wppkp2OprR4pS3nlBpPvyL2GV9cqdc= cloud.google.com/go/pubsub v1.17.1/go.mod h1:4qDxMr1WsM9+aQAz36ltDwCIM+R0QdlseyFjBuNvnss= +cloud.google.com/go/secretmanager v1.4.0 h1:Cl+kDYvKHjPQ1l2DZDr2FG/cXUzNGCZkh05BARgddo8= +cloud.google.com/go/secretmanager v1.4.0/go.mod h1:h2VZz7Svt1W9/YVl7mfcX9LddvS6SOLOvMoOXBhYT1k= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= @@ -3211,6 +3214,7 @@ google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2 google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220405205423-9d709892a2bf/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac h1:qSNTkEN+L2mvWcLgJOR+8bdHX9rN/IdU3A1Ghpfb1Rg= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= diff --git a/sg.config.yaml b/sg.config.yaml index 29cf704ffbe..64e1993f4e2 100644 --- a/sg.config.yaml +++ b/sg.config.yaml @@ -749,6 +749,13 @@ commandsets: - zoekt-webserver-0 - zoekt-webserver-1 + enterprise-e2e: + <<: *enterprise_set + env: + # EXTSVC_CONFIG_FILE being set prevents the e2e test suite to add + # additional connections. + EXTSVC_CONFIG_FILE: "" + dotcom: <<: *enterprise_set env: @@ -960,7 +967,23 @@ tests: cmd: yarn run jest --testPathIgnorePatterns end-to-end regression integration storybook frontend-e2e: - cmd: yarn run test-e2e + preamble: | + sg start enterprise-e2e must be already running for these tests to work. + + If you run into authentication issues, you can run the following commands to fix them: + sg db reset-pg && sg db add-user -name test@sourcegraph.com -password supersecurepassword + cmd: | + yarn run test-e2e + env: + TEST_USER_EMAIL: test@sourcegraph.com + TEST_USER_PASSWORD: supersecurepassword + SOURCEGRAPH_BASE_URL: https://sourcegraph.test:3443 + BROWSER: chrome + secrets: + GH_TOKEN: + provider: "gcloud" + project: "sourcegraph-ci" + name: "BUILDKITE_GITHUBDOTCOM_TOKEN" docsite: cmd: .bin/docsite_${DOCSITE_VERSION} check ./doc