Compare commits

...

3 Commits

Author SHA1 Message Date
Aaron Lehmann 11fbf1a607
Merge 5b21a0e271 into 15157c1c40 2024-04-26 17:48:45 +00:00
Colin Adler 15157c1c40
chore: add network integration test suite scaffolding (#13072)
* chore: add network integration test suite scaffolding

* dean comments
2024-04-26 17:48:41 +00:00
Aaron Lehmann 5b21a0e271 feat: influence parameter defaults through cli flag/env
Add a --parameter-default flag / CODER_RICH_PARAMETER_DEFAULT
environment variable which overrides default values suggested for
parameters.

This allows scripts or middleware wrapping the CLI to substitute
defaults for parameter values beyond those defined at the template
level. For example, Git repository/branch parameters can be given
defaults based on the current checkout, or default parameter values can
be parsed out of files inside the repo.
2024-04-22 16:48:44 -07:00
16 changed files with 498 additions and 22 deletions

View File

@ -10,7 +10,7 @@ import (
"github.com/coder/serpent"
)
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter) (string, error) {
func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.TemplateVersionParameter, defaults map[string]string) (string, error) {
label := templateVersionParameter.Name
if templateVersionParameter.DisplayName != "" {
label = templateVersionParameter.DisplayName
@ -26,6 +26,11 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
_, _ = fmt.Fprintln(inv.Stdout, " "+strings.TrimSpace(strings.Join(strings.Split(templateVersionParameter.DescriptionPlaintext, "\n"), "\n "))+"\n")
}
defaultValue := templateVersionParameter.DefaultValue
if v, ok := defaults[templateVersionParameter.Name]; ok {
defaultValue = v
}
var err error
var value string
if templateVersionParameter.Type == "list(string)" {
@ -58,7 +63,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
var richParameterOption *codersdk.TemplateVersionParameterOption
richParameterOption, err = RichSelect(inv, RichSelectOptions{
Options: templateVersionParameter.Options,
Default: templateVersionParameter.DefaultValue,
Default: defaultValue,
HideSearch: true,
})
if err == nil {
@ -69,7 +74,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
} else {
text := "Enter a value"
if !templateVersionParameter.Required {
text += fmt.Sprintf(" (default: %q)", templateVersionParameter.DefaultValue)
text += fmt.Sprintf(" (default: %q)", defaultValue)
}
text += ":"
@ -87,7 +92,7 @@ func RichParameter(inv *serpent.Invocation, templateVersionParameter codersdk.Te
// If they didn't specify anything, use the default value if set.
if len(templateVersionParameter.Options) == 0 && value == "" {
value = templateVersionParameter.DefaultValue
value = defaultValue
}
return value, nil

View File

@ -165,6 +165,11 @@ func (r *RootCmd) create() *serpent.Command {
return xerrors.Errorf("can't parse given parameter values: %w", err)
}
cliBuildParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return xerrors.Errorf("can't parse given parameter defaults: %w", err)
}
var sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
if copyParametersFrom != "" {
sourceWorkspaceParameters, err = client.WorkspaceBuildParameters(inv.Context(), sourceWorkspace.LatestBuild.ID)
@ -178,8 +183,9 @@ func (r *RootCmd) create() *serpent.Command {
TemplateVersionID: templateVersionID,
NewWorkspaceName: workspaceName,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameters: cliBuildParameters,
RichParameterDefaults: cliBuildParameterDefaults,
SourceWorkspaceParameters: sourceWorkspaceParameters,
})
@ -262,6 +268,7 @@ func (r *RootCmd) create() *serpent.Command {
cliui.SkipPromptOption(),
)
cmd.Options = append(cmd.Options, parameterFlags.cliParameters()...)
cmd.Options = append(cmd.Options, parameterFlags.cliParameterDefaults()...)
return cmd
}
@ -276,9 +283,10 @@ type prepWorkspaceBuildArgs struct {
PromptBuildOptions bool
BuildOptions []codersdk.WorkspaceBuildParameter
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
PromptRichParameters bool
RichParameters []codersdk.WorkspaceBuildParameter
RichParameterFile string
RichParameterDefaults []codersdk.WorkspaceBuildParameter
}
// prepWorkspaceBuild will ensure a workspace build will succeed on the latest template version.
@ -311,7 +319,8 @@ func prepWorkspaceBuild(inv *serpent.Invocation, client *codersdk.Client, args p
WithBuildOptions(args.BuildOptions).
WithPromptRichParameters(args.PromptRichParameters).
WithRichParameters(args.RichParameters).
WithRichParametersFile(parameterFile)
WithRichParametersFile(parameterFile).
WithRichParametersDefaults(args.RichParameterDefaults)
buildParameters, err := resolver.Resolve(inv, args.Action, templateVersionParameters)
if err != nil {
return nil, err

View File

@ -315,6 +315,68 @@ func TestCreateWithRichParameters(t *testing.T) {
<-doneChan
})
t.Run("ParametersDefaults", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
inv, root := clitest.New(t, "create", "my-workspace", "--template", template.Name,
"--parameter-default", fmt.Sprintf("%s=%s", firstParameterName, firstParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", secondParameterName, secondParameterValue),
"--parameter-default", fmt.Sprintf("%s=%s", immutableParameterName, immutableParameterValue))
clitest.SetupConfig(t, member, root)
doneChan := make(chan struct{})
pty := ptytest.New(t).Attach(inv)
go func() {
defer close(doneChan)
err := inv.Run()
assert.NoError(t, err)
}()
matches := []string{
firstParameterDescription, firstParameterValue,
secondParameterDescription, secondParameterValue,
immutableParameterDescription, immutableParameterValue,
}
for i := 0; i < len(matches); i += 2 {
match := matches[i]
defaultValue := matches[i+1]
pty.ExpectMatch(match)
pty.ExpectMatch(`Enter a value (default: "` + defaultValue + `")`)
pty.WriteLine("")
}
pty.ExpectMatch("Confirm create?")
pty.WriteLine("yes")
<-doneChan
// Verify that the expected default values were used.
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitShort)
defer cancel()
workspaces, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
Name: "my-workspace",
})
require.NoError(t, err, "can't list available workspaces")
require.Len(t, workspaces.Workspaces, 1)
workspaceLatestBuild := workspaces.Workspaces[0].LatestBuild
require.Equal(t, version.ID, workspaceLatestBuild.TemplateVersionID)
buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceLatestBuild.ID)
require.NoError(t, err)
require.Len(t, buildParameters, 3)
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: firstParameterName, Value: firstParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: secondParameterName, Value: secondParameterValue})
require.Contains(t, buildParameters, codersdk.WorkspaceBuildParameter{Name: immutableParameterName, Value: immutableParameterValue})
})
t.Run("RichParametersFile", func(t *testing.T) {
t.Parallel()

View File

@ -18,14 +18,16 @@ type workspaceParameterFlags struct {
promptBuildOptions bool
buildOptions []string
richParameterFile string
richParameters []string
richParameterFile string
richParameters []string
richParameterDefaults []string
promptRichParameters bool
}
func (wpf *workspaceParameterFlags) allOptions() []serpent.Option {
options := append(wpf.cliBuildOptions(), wpf.cliParameters()...)
options = append(options, wpf.cliParameterDefaults()...)
return append(options, wpf.alwaysPrompt())
}
@ -62,6 +64,17 @@ func (wpf *workspaceParameterFlags) cliParameters() []serpent.Option {
}
}
func (wpf *workspaceParameterFlags) cliParameterDefaults() []serpent.Option {
return serpent.OptionSet{
serpent.Option{
Flag: "parameter-default",
Env: "CODER_RICH_PARAMETER_DEFAULT",
Description: `Rich parameter default values in the format "name=value".`,
Value: serpent.StringArrayOf(&wpf.richParameterDefaults),
},
}
}
func (wpf *workspaceParameterFlags) alwaysPrompt() serpent.Option {
return serpent.Option{
Flag: "always-prompt",

View File

@ -26,9 +26,10 @@ type ParameterResolver struct {
lastBuildParameters []codersdk.WorkspaceBuildParameter
sourceWorkspaceParameters []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
richParameters []codersdk.WorkspaceBuildParameter
richParametersDefaults map[string]string
richParametersFile map[string]string
buildOptions []codersdk.WorkspaceBuildParameter
promptRichParameters bool
promptBuildOptions bool
@ -59,6 +60,16 @@ func (pr *ParameterResolver) WithRichParametersFile(fileMap map[string]string) *
return pr
}
func (pr *ParameterResolver) WithRichParametersDefaults(params []codersdk.WorkspaceBuildParameter) *ParameterResolver {
if pr.richParametersDefaults == nil {
pr.richParametersDefaults = make(map[string]string)
}
for _, p := range params {
pr.richParametersDefaults[p.Name] = p.Value
}
return pr
}
func (pr *ParameterResolver) WithPromptRichParameters(promptRichParameters bool) *ParameterResolver {
pr.promptRichParameters = promptRichParameters
return pr
@ -227,7 +238,7 @@ func (pr *ParameterResolver) resolveWithInput(resolved []codersdk.WorkspaceBuild
(action == WorkspaceUpdate && tvp.Mutable && tvp.Required) ||
(action == WorkspaceUpdate && !tvp.Mutable && firstTimeUse) ||
(tvp.Mutable && !tvp.Ephemeral && pr.promptRichParameters) {
parameterValue, err := cliui.RichParameter(inv, tvp)
parameterValue, err := cliui.RichParameter(inv, tvp, pr.richParametersDefaults)
if err != nil {
return nil, err
}

View File

@ -99,7 +99,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
cliRichParameters, err := asWorkspaceBuildParameters(parameterFlags.richParameters)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse build options: %w", err)
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameters: %w", err)
}
cliRichParameterDefaults, err := asWorkspaceBuildParameters(parameterFlags.richParameterDefaults)
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, xerrors.Errorf("unable to parse rich parameter defaults: %w", err)
}
buildParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
@ -108,11 +113,12 @@ func buildWorkspaceStartRequest(inv *serpent.Invocation, client *codersdk.Client
NewWorkspaceName: workspace.Name,
LastBuildParameters: lastBuildParameters,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
PromptBuildOptions: parameterFlags.promptBuildOptions,
BuildOptions: buildOptions,
PromptRichParameters: parameterFlags.promptRichParameters,
RichParameters: cliRichParameters,
RichParameterFile: parameterFlags.richParameterFile,
RichParameterDefaults: cliRichParameterDefaults,
})
if err != nil {
return codersdk.CreateWorkspaceBuildRequest{}, err

View File

@ -20,6 +20,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -19,6 +19,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

View File

@ -21,6 +21,9 @@ OPTIONS:
--parameter string-array, $CODER_RICH_PARAMETER
Rich parameter value in the format "name=value".
--parameter-default string-array, $CODER_RICH_PARAMETER_DEFAULT
Rich parameter default values in the format "name=value".
--rich-parameter-file string, $CODER_RICH_PARAMETER_FILE
Specify a file path with values for rich parameters defined in the
template.

9
docs/cli/create.md generated
View File

@ -91,3 +91,12 @@ Rich parameter value in the format "name=value".
| Environment | <code>$CODER_RICH_PARAMETER_FILE</code> |
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".

9
docs/cli/restart.md generated
View File

@ -55,6 +55,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

9
docs/cli/start.md generated
View File

@ -55,6 +55,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

9
docs/cli/update.md generated
View File

@ -53,6 +53,15 @@ Rich parameter value in the format "name=value".
Specify a file path with values for rich parameters defined in the template.
### --parameter-default
| | |
| ----------- | ------------------------------------------ |
| Type | <code>string-array</code> |
| Environment | <code>$CODER_RICH_PARAMETER_DEFAULT</code> |
Rich parameter default values in the format "name=value".
### --always-prompt
| | |

View File

@ -0,0 +1,128 @@
package integration
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"net/netip"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"golang.org/x/xerrors"
"nhooyr.io/websocket"
"tailscale.com/tailcfg"
"cdr.dev/slog"
"github.com/coder/coder/v2/coderd/httpapi"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
)
func NetworkSetupDefault(*testing.T) {}
func DERPMapTailscale(ctx context.Context, t *testing.T) *tailcfg.DERPMap {
ctx, cancel := context.WithTimeout(ctx, testutil.WaitShort)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", "https://controlplane.tailscale.com/derpmap/default", nil)
require.NoError(t, err)
res, err := http.DefaultClient.Do(req)
require.NoError(t, err)
defer res.Body.Close()
dm := &tailcfg.DERPMap{}
dec := json.NewDecoder(res.Body)
err = dec.Decode(dm)
require.NoError(t, err)
return dm
}
func CoordinatorInMemory(t *testing.T, logger slog.Logger, dm *tailcfg.DERPMap) (coord tailnet.Coordinator, url string) {
coord = tailnet.NewCoordinator(logger)
var coordPtr atomic.Pointer[tailnet.Coordinator]
coordPtr.Store(&coord)
t.Cleanup(func() { _ = coord.Close() })
csvc, err := tailnet.NewClientService(logger, &coordPtr, 10*time.Minute, func() *tailcfg.DERPMap {
return dm
})
require.NoError(t, err)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
idStr := strings.TrimPrefix(r.URL.Path, "/")
id, err := uuid.Parse(idStr)
if err != nil {
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
Message: "Bad agent id.",
Detail: err.Error(),
})
return
}
conn, err := websocket.Accept(w, r, nil)
if err != nil {
httpapi.Write(r.Context(), w, http.StatusBadRequest, codersdk.Response{
Message: "Failed to accept websocket.",
Detail: err.Error(),
})
return
}
ctx, wsNetConn := codersdk.WebsocketNetConn(r.Context(), conn, websocket.MessageBinary)
defer wsNetConn.Close()
err = csvc.ServeConnV2(ctx, wsNetConn, tailnet.StreamID{
Name: "client-" + id.String(),
ID: id,
Auth: tailnet.SingleTailnetCoordinateeAuth{},
})
if err != nil && !xerrors.Is(err, io.EOF) && !xerrors.Is(err, context.Canceled) {
_ = conn.Close(websocket.StatusInternalError, err.Error())
return
}
}))
t.Cleanup(srv.Close)
return coord, srv.URL
}
func TailnetSetupDRPC(ctx context.Context, t *testing.T, logger slog.Logger,
id, agentID uuid.UUID,
coordinateURL string,
dm *tailcfg.DERPMap,
) *tailnet.Conn {
ip := tailnet.IPFromUUID(id)
conn, err := tailnet.NewConn(&tailnet.Options{
Addresses: []netip.Prefix{netip.PrefixFrom(ip, 128)},
DERPMap: dm,
Logger: logger,
})
require.NoError(t, err)
t.Cleanup(func() { _ = conn.Close() })
//nolint:bodyclose
ws, _, err := websocket.Dial(ctx, coordinateURL+"/"+id.String(), nil)
require.NoError(t, err)
client, err := tailnet.NewDRPCClient(
websocket.NetConn(ctx, ws, websocket.MessageBinary),
logger,
)
require.NoError(t, err)
coord, err := client.Coordinate(ctx)
require.NoError(t, err)
coordination := tailnet.NewRemoteCoordination(logger, coord, conn, agentID)
t.Cleanup(func() { _ = coordination.Close() })
return conn
}

View File

@ -0,0 +1,194 @@
package integration
import (
"context"
"flag"
"fmt"
"os"
"os/exec"
"strconv"
"syscall"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"tailscale.com/tailcfg"
"cdr.dev/slog"
"cdr.dev/slog/sloggers/slogtest"
"github.com/coder/coder/v2/tailnet"
"github.com/coder/coder/v2/testutil"
)
var (
isChild = flag.Bool("child", false, "Run tests as a child")
childTestID = flag.Int("child-test-id", 0, "Which test is being run")
childCoordinateURL = flag.String("child-coordinate-url", "", "The coordinate url to connect back to")
childAgentID = flag.String("child-agent-id", "", "The agent id of the child")
)
func TestMain(m *testing.M) {
if run := os.Getenv("CODER_TAILNET_TESTS"); run == "" {
_, _ = fmt.Println("skipping tests...")
return
}
if os.Getuid() != 0 {
_, _ = fmt.Println("networking integration tests must run as root")
return
}
flag.Parse()
os.Exit(m.Run())
}
var tests = []Test{{
Name: "Normal",
DERPMap: DERPMapTailscale,
Coordinator: CoordinatorInMemory,
Parent: Parent{
NetworkSetup: NetworkSetupDefault,
TailnetSetup: TailnetSetupDRPC,
Run: func(ctx context.Context, t *testing.T, opts ParentOpts) {
reach := opts.Conn.AwaitReachable(ctx, tailnet.IPFromUUID(opts.AgentID))
assert.True(t, reach)
},
},
Child: Child{
NetworkSetup: NetworkSetupDefault,
TailnetSetup: TailnetSetupDRPC,
Run: func(ctx context.Context, t *testing.T, opts ChildOpts) {
// wait until the parent kills us
<-make(chan struct{})
},
},
}}
//nolint:paralleltest
func TestIntegration(t *testing.T) {
if *isChild {
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
t.Cleanup(cancel)
agentID, err := uuid.Parse(*childAgentID)
require.NoError(t, err)
test := tests[*childTestID]
test.Child.NetworkSetup(t)
dm := test.DERPMap(ctx, t)
conn := test.Child.TailnetSetup(ctx, t, logger, agentID, uuid.Nil, *childCoordinateURL, dm)
test.Child.Run(ctx, t, ChildOpts{
Logger: logger,
Conn: conn,
AgentID: agentID,
})
return
}
for id, test := range tests {
t.Run(test.Name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitSuperLong)
t.Cleanup(cancel)
logger := slogtest.Make(t, nil).Leveled(slog.LevelDebug)
parentID, childID := uuid.New(), uuid.New()
dm := test.DERPMap(ctx, t)
_, coordURL := test.Coordinator(t, logger, dm)
child, waitChild := execChild(ctx, id, coordURL, childID)
test.Parent.NetworkSetup(t)
conn := test.Parent.TailnetSetup(ctx, t, logger, parentID, childID, coordURL, dm)
test.Parent.Run(ctx, t, ParentOpts{
Logger: logger,
Conn: conn,
ClientID: parentID,
AgentID: childID,
})
child.Process.Signal(syscall.SIGINT)
<-waitChild
})
}
}
type Test struct {
// Name is the name of the test.
Name string
// DERPMap returns the DERP map to use for both the parent and child. It is
// called once at the beginning of the test.
DERPMap func(ctx context.Context, t *testing.T) *tailcfg.DERPMap
// Coordinator returns a running tailnet coordinator, and the url to reach
// it on.
Coordinator func(t *testing.T, logger slog.Logger, dm *tailcfg.DERPMap) (coord tailnet.Coordinator, url string)
Parent Parent
Child Child
}
// Parent is the struct containing all of the parent specific configurations.
// Functions are invoked in order of struct definition.
type Parent struct {
// NetworkSetup is run before all test code. It can be used to setup
// networking scenarios.
NetworkSetup func(t *testing.T)
// TailnetSetup creates a tailnet network.
TailnetSetup func(
ctx context.Context, t *testing.T, logger slog.Logger,
id, agentID uuid.UUID, coordURL string, dm *tailcfg.DERPMap,
) *tailnet.Conn
Run func(ctx context.Context, t *testing.T, opts ParentOpts)
}
// Child is the struct containing all of the child specific configurations.
// Functions are invoked in order of struct definition.
type Child struct {
// NetworkSetup is run before all test code. It can be used to setup
// networking scenarios.
NetworkSetup func(t *testing.T)
// TailnetSetup creates a tailnet network.
TailnetSetup func(
ctx context.Context, t *testing.T, logger slog.Logger,
id, agentID uuid.UUID, coordURL string, dm *tailcfg.DERPMap,
) *tailnet.Conn
// Run runs the actual test. Parents and children run in separate processes,
// so it's important to ensure no communication happens over memory between
// run functions of parents and children.
Run func(ctx context.Context, t *testing.T, opts ChildOpts)
}
type ParentOpts struct {
Logger slog.Logger
Conn *tailnet.Conn
ClientID uuid.UUID
AgentID uuid.UUID
}
type ChildOpts struct {
Logger slog.Logger
Conn *tailnet.Conn
AgentID uuid.UUID
}
func execChild(ctx context.Context, testID int, coordURL string, agentID uuid.UUID) (*exec.Cmd, <-chan error) {
ch := make(chan error)
binary := os.Args[0]
args := os.Args[1:]
args = append(args,
"--child=true",
"--child-test-id="+strconv.Itoa(testID),
"--child-coordinate-url="+coordURL,
"--child-agent-id="+agentID.String(),
)
cmd := exec.CommandContext(ctx, binary, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
go func() {
ch <- cmd.Run()
}()
return cmd, ch
}