sg: create a test command to run e2e tests locally (#34627)

This commit is contained in:
Jean-Hadrien Chabran 2022-05-04 09:12:30 +02:00 committed by GitHub
parent a2d1fd474d
commit 284aba024b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 105 additions and 12 deletions

View File

@ -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

View File

@ -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

3
go.mod
View File

@ -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

6
go.sum
View File

@ -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=

View File

@ -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