From 19b4323512b8b1759ee8b5ca58286c01d2ce0062 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 11 Apr 2022 16:06:15 -0500 Subject: [PATCH] feat: Allow workspace resources to attach multiple agents (#942) This enables a "kubernetes_pod" to attach multiple agents that could be for multiple services. Each agent is required to have a unique name, so SSH syntax is: `coder ssh .` A resource can have zero agents too, they aren't required. --- .vscode/settings.json | 2 +- Makefile | 5 +- cli/cliui/agent.go | 16 +- cli/cliui/agent_test.go | 12 +- cli/configssh.go | 34 +- cli/gitssh_test.go | 6 +- cli/ssh.go | 52 ++- cli/ssh_test.go | 8 +- cli/templates.go | 2 +- cli/workspaceagent_test.go | 12 +- cli/workspaceshow.go | 23 -- cmd/cliui/main.go | 14 +- cmd/templater/main.go | 17 +- coderd/coderd.go | 27 +- coderd/coderdtest/coderdtest.go | 9 +- coderd/database/databasefake/databasefake.go | 33 +- coderd/database/dump.sql | 6 +- coderd/database/migrations/000004_jobs.up.sql | 4 +- coderd/database/models.go | 4 +- coderd/database/querier.go | 3 +- coderd/database/queries.sql.go | 134 +++++-- coderd/database/queries/workspaceagents.sql | 17 +- .../database/queries/workspaceresources.sql | 5 +- coderd/gitsshkey_test.go | 75 ++-- coderd/httpmw/workspaceagentparam.go | 94 +++++ coderd/httpmw/workspaceagentparam_test.go | 153 ++++++++ coderd/provisionerdaemons.go | 27 +- coderd/provisionerjobs.go | 47 ++- coderd/templateversions_test.go | 10 +- coderd/workspaceagents.go | 257 +++++++++++++ coderd/workspaceagents_test.go | 108 ++++++ coderd/workspacebuilds.go | 4 +- coderd/workspacebuilds_test.go | 10 +- coderd/workspaceresourceauth_test.go | 8 +- coderd/workspaceresources.go | 269 +------------- coderd/workspaceresources_test.go | 62 +--- codersdk/gitsshkey.go | 2 +- ...paceresourceauth.go => workspaceagents.go} | 121 +++++- codersdk/workspaceresources.go | 108 +----- examples/aws-linux/main.tf | 17 +- examples/aws-windows/main.tf | 15 +- examples/gcp-linux/main.tf | 34 +- examples/gcp-windows/main.tf | 30 +- provisioner/terraform/provision.go | 180 ++++++--- provisioner/terraform/provision_test.go | 153 ++++---- provisionersdk/proto/provisioner.pb.go | 346 ++++++++++-------- provisionersdk/proto/provisioner.proto | 15 +- 47 files changed, 1550 insertions(+), 1040 deletions(-) create mode 100644 coderd/httpmw/workspaceagentparam.go create mode 100644 coderd/httpmw/workspaceagentparam_test.go create mode 100644 coderd/workspaceagents.go create mode 100644 coderd/workspaceagents_test.go rename codersdk/{workspaceresourceauth.go => workspaceagents.go} (51%) diff --git a/.vscode/settings.json b/.vscode/settings.json index 38a091b323..2988daa9f0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -62,7 +62,7 @@ "emeraldwalk.runonsave": { "commands": [ { - "match": "database/query.sql", + "match": "database/queries/*.sql", "cmd": "make gen" } ] diff --git a/Makefile b/Makefile index 91baebe6cc..681d565a22 100644 --- a/Makefile +++ b/Makefile @@ -42,7 +42,10 @@ fmt/sql: $(wildcard coderd/database/queries/*.sql) sed -i 's/@ /@/g' ./coderd/database/queries/*.sql -fmt: fmt/prettier fmt/sql +fmt/terraform: $(wildcard *.tf) + terraform fmt -recursive + +fmt: fmt/prettier fmt/sql fmt/terraform .PHONY: fmt gen: coderd/database/generate peerbroker/proto provisionersdk/proto provisionerd/proto diff --git a/cli/cliui/agent.go b/cli/cliui/agent.go index ca32fd3270..3770d07d5a 100644 --- a/cli/cliui/agent.go +++ b/cli/cliui/agent.go @@ -15,7 +15,7 @@ import ( type AgentOptions struct { WorkspaceName string - Fetch func(context.Context) (codersdk.WorkspaceResource, error) + Fetch func(context.Context) (codersdk.WorkspaceAgent, error) FetchInterval time.Duration WarnInterval time.Duration } @@ -29,20 +29,20 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { opts.WarnInterval = 30 * time.Second } var resourceMutex sync.Mutex - resource, err := opts.Fetch(ctx) + agent, err := opts.Fetch(ctx) if err != nil { return xerrors.Errorf("fetch: %w", err) } - if resource.Agent.Status == codersdk.WorkspaceAgentConnected { + if agent.Status == codersdk.WorkspaceAgentConnected { return nil } - if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected { + if agent.Status == codersdk.WorkspaceAgentDisconnected { opts.WarnInterval = 0 } spin := spinner.New(spinner.CharSets[78], 100*time.Millisecond, spinner.WithColor("fgHiGreen")) spin.Writer = writer spin.ForceOutput = true - spin.Suffix = " Waiting for connection from " + Styles.Field.Render(resource.Type+"."+resource.Name) + "..." + spin.Suffix = " Waiting for connection from " + Styles.Field.Render(agent.Name) + "..." spin.Start() defer spin.Stop() @@ -59,7 +59,7 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { resourceMutex.Lock() defer resourceMutex.Unlock() message := "Don't panic, your workspace is booting up!" - if resource.Agent.Status == codersdk.WorkspaceAgentDisconnected { + if agent.Status == codersdk.WorkspaceAgentDisconnected { message = "The workspace agent lost connection! Wait for it to reconnect or run: " + Styles.Code.Render("coder workspaces rebuild "+opts.WorkspaceName) } // This saves the cursor position, then defers clearing from the cursor @@ -74,11 +74,11 @@ func Agent(ctx context.Context, writer io.Writer, opts AgentOptions) error { case <-ticker.C: } resourceMutex.Lock() - resource, err = opts.Fetch(ctx) + agent, err = opts.Fetch(ctx) if err != nil { return xerrors.Errorf("fetch: %w", err) } - if resource.Agent.Status != codersdk.WorkspaceAgentConnected { + if agent.Status != codersdk.WorkspaceAgentConnected { resourceMutex.Unlock() continue } diff --git a/cli/cliui/agent_test.go b/cli/cliui/agent_test.go index 87323c17a6..6f717541ed 100644 --- a/cli/cliui/agent_test.go +++ b/cli/cliui/agent_test.go @@ -22,16 +22,14 @@ func TestAgent(t *testing.T) { RunE: func(cmd *cobra.Command, args []string) error { err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{ WorkspaceName: "example", - Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) { - resource := codersdk.WorkspaceResource{ - Agent: &codersdk.WorkspaceAgent{ - Status: codersdk.WorkspaceAgentDisconnected, - }, + Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) { + agent := codersdk.WorkspaceAgent{ + Status: codersdk.WorkspaceAgentDisconnected, } if disconnected.Load() { - resource.Agent.Status = codersdk.WorkspaceAgentConnected + agent.Status = codersdk.WorkspaceAgentConnected } - return resource, nil + return agent, nil }, FetchInterval: time.Millisecond, WarnInterval: 10 * time.Millisecond, diff --git a/cli/configssh.go b/cli/configssh.go index d5c2dca40f..6bcd117372 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -15,6 +15,7 @@ import ( "github.com/coder/coder/cli/cliflag" "github.com/coder/coder/cli/cliui" + "github.com/coder/coder/coderd/database" "github.com/coder/coder/codersdk" ) @@ -70,29 +71,30 @@ func configSSH() *cobra.Command { for _, workspace := range workspaces { workspace := workspace errGroup.Go(func() error { - resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID) + resources, err := client.TemplateVersionResources(cmd.Context(), workspace.LatestBuild.TemplateVersionID) if err != nil { return err } - resourcesWithAgents := make([]codersdk.WorkspaceResource, 0) for _, resource := range resources { - if resource.Agent == nil { + if resource.Transition != database.WorkspaceTransitionStart { continue } - resourcesWithAgents = append(resourcesWithAgents, resource) + for _, agent := range resource.Agents { + sshConfigContentMutex.Lock() + hostname := workspace.Name + if len(resource.Agents) > 1 { + hostname += "." + agent.Name + } + sshConfigContent += strings.Join([]string{ + "Host coder." + hostname, + "\tHostName coder." + hostname, + fmt.Sprintf("\tProxyCommand %q ssh --stdio %s", binPath, hostname), + "\tConnectTimeout=0", + "\tStrictHostKeyChecking=no", + }, "\n") + "\n" + sshConfigContentMutex.Unlock() + } } - sshConfigContentMutex.Lock() - defer sshConfigContentMutex.Unlock() - if len(resourcesWithAgents) == 1 { - sshConfigContent += strings.Join([]string{ - "Host coder." + workspace.Name, - "\tHostName coder." + workspace.Name, - fmt.Sprintf("\tProxyCommand %q ssh --stdio %s", binPath, workspace.Name), - "\tConnectTimeout=0", - "\tStrictHostKeyChecking=no", - }, "\n") + "\n" - } - return nil }) } diff --git a/cli/gitssh_test.go b/cli/gitssh_test.go index d9b36d103f..4ad6f6daea 100644 --- a/cli/gitssh_test.go +++ b/cli/gitssh_test.go @@ -49,11 +49,11 @@ func TestGitSSH(t *testing.T) { Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, - }, + }}, }}, }, }, @@ -81,7 +81,7 @@ func TestGitSSH(t *testing.T) { coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID) require.NoError(t, err) - dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil) + dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil) require.NoError(t, err) defer dialer.Close() _, err = dialer.Ping() diff --git a/cli/ssh.go b/cli/ssh.go index e8310c53a2..6cdda44238 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -7,6 +7,7 @@ import ( "os" "time" + "github.com/google/uuid" "github.com/mattn/go-isatty" "github.com/pion/webrtc/v3" "github.com/spf13/cobra" @@ -25,7 +26,7 @@ func ssh() *cobra.Command { stdio bool ) cmd := &cobra.Command{ - Use: "ssh [resource]", + Use: "ssh [agent]", RunE: func(cmd *cobra.Command, args []string) error { client, err := createClient(cmd) if err != nil { @@ -57,50 +58,45 @@ func ssh() *cobra.Command { return err } - resourceByAddress := make(map[string]codersdk.WorkspaceResource) + agents := make([]codersdk.WorkspaceAgent, 0) for _, resource := range resources { - if resource.Agent == nil { - continue - } - resourceByAddress[resource.Address] = resource + agents = append(agents, resource.Agents...) } - - var resourceAddress string + if len(agents) == 0 { + return xerrors.New("workspace has no agents") + } + var agent codersdk.WorkspaceAgent if len(args) >= 2 { - resourceAddress = args[1] - } else { - // No resource name was provided! - if len(resourceByAddress) > 1 { - // List available resources to connect into? - return xerrors.Errorf("multiple agents") - } - for _, resource := range resourceByAddress { - resourceAddress = resource.Address + for _, otherAgent := range agents { + if otherAgent.Name != args[1] { + continue + } + agent = otherAgent break } - } - - resource, exists := resourceByAddress[resourceAddress] - if !exists { - resourceKeys := make([]string, 0) - for resourceKey := range resourceByAddress { - resourceKeys = append(resourceKeys, resourceKey) + if agent.ID == uuid.Nil { + return xerrors.Errorf("agent not found by name %q", args[1]) } - return xerrors.Errorf("no sshable agent with address %q: %+v", resourceAddress, resourceKeys) + } + if agent.ID == uuid.Nil { + if len(agents) > 1 { + return xerrors.New("you must specify the name of an agent") + } + agent = agents[0] } // OpenSSH passes stderr directly to the calling TTY. // This is required in "stdio" mode so a connecting indicator can be displayed. err = cliui.Agent(cmd.Context(), cmd.ErrOrStderr(), cliui.AgentOptions{ WorkspaceName: workspace.Name, - Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) { - return client.WorkspaceResource(ctx, resource.ID) + Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) { + return client.WorkspaceAgent(ctx, agent.ID) }, }) if err != nil { return xerrors.Errorf("await agent: %w", err) } - conn, err := client.DialWorkspaceAgent(cmd.Context(), resource.ID, []webrtc.ICEServer{{ + conn, err := client.DialWorkspaceAgent(cmd.Context(), agent.ID, []webrtc.ICEServer{{ URLs: []string{"stun:stun.l.google.com:19302"}, }}, nil) if err != nil { diff --git a/cli/ssh_test.go b/cli/ssh_test.go index cc192150fd..208d8379d0 100644 --- a/cli/ssh_test.go +++ b/cli/ssh_test.go @@ -40,12 +40,12 @@ func TestSSH(t *testing.T) { Resources: []*proto.Resource{{ Name: "dev", Type: "google_compute_instance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: uuid.NewString(), Auth: &proto.Agent_Token{ Token: agentToken, }, - }, + }}, }}, }, }, @@ -98,12 +98,12 @@ func TestSSH(t *testing.T) { Resources: []*proto.Resource{{ Name: "dev", Type: "google_compute_instance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: uuid.NewString(), Auth: &proto.Agent_Token{ Token: agentToken, }, - }, + }}, }}, }, }, diff --git a/cli/templates.go b/cli/templates.go index 340d000d03..e1f65ca4b1 100644 --- a/cli/templates.go +++ b/cli/templates.go @@ -74,7 +74,7 @@ func displayTemplateVersionInfo(cmd *cobra.Command, resources []codersdk.Workspa } else { _, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Keyword.Render("+ start")+cliui.Styles.Placeholder.Render(" (deletes on stop)")) } - if resource.Agent != nil { + if len(resource.Agents) > 0 { _, _ = fmt.Fprintln(cmd.OutOrStdout(), " "+cliui.Styles.Fuschia.Render("▲ allows ssh")) } _, _ = fmt.Fprintln(cmd.OutOrStdout()) diff --git a/cli/workspaceagent_test.go b/cli/workspaceagent_test.go index abad44d13a..c672c7bb79 100644 --- a/cli/workspaceagent_test.go +++ b/cli/workspaceagent_test.go @@ -32,11 +32,11 @@ func TestWorkspaceAgent(t *testing.T) { Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, - }, + }}, }}, }, }, @@ -61,7 +61,7 @@ func TestWorkspaceAgent(t *testing.T) { coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID) require.NoError(t, err) - dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil) + dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil) require.NoError(t, err) defer dialer.Close() _, err = dialer.Ping() @@ -86,11 +86,11 @@ func TestWorkspaceAgent(t *testing.T) { Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, - }, + }}, }}, }, }, @@ -115,7 +115,7 @@ func TestWorkspaceAgent(t *testing.T) { coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) resources, err := client.WorkspaceResourcesByBuild(ctx, workspace.LatestBuild.ID) require.NoError(t, err) - dialer, err := client.DialWorkspaceAgent(ctx, resources[0].ID, nil, nil) + dialer, err := client.DialWorkspaceAgent(ctx, resources[0].Agents[0].ID, nil, nil) require.NoError(t, err) defer dialer.Close() _, err = dialer.Ping() diff --git a/cli/workspaceshow.go b/cli/workspaceshow.go index 80a2fcc3ca..36a7b50404 100644 --- a/cli/workspaceshow.go +++ b/cli/workspaceshow.go @@ -1,36 +1,13 @@ package cli import ( - "fmt" - "github.com/spf13/cobra" - - "github.com/coder/coder/codersdk" ) func workspaceShow() *cobra.Command { return &cobra.Command{ Use: "show", RunE: func(cmd *cobra.Command, args []string) error { - client, err := createClient(cmd) - if err != nil { - return err - } - workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0]) - if err != nil { - return err - } - resources, err := client.WorkspaceResourcesByBuild(cmd.Context(), workspace.LatestBuild.ID) - if err != nil { - return err - } - for _, resource := range resources { - if resource.Agent == nil { - continue - } - - _, _ = fmt.Printf("Agent: %+v\n", resource.Agent) - } return nil }, } diff --git a/cmd/cliui/main.go b/cmd/cliui/main.go index c633a6b6d2..391e76a35c 100644 --- a/cmd/cliui/main.go +++ b/cmd/cliui/main.go @@ -161,21 +161,17 @@ func main() { root.AddCommand(&cobra.Command{ Use: "agent", RunE: func(cmd *cobra.Command, args []string) error { - resource := codersdk.WorkspaceResource{ - Type: "google_compute_instance", - Name: "dev", - Agent: &codersdk.WorkspaceAgent{ - Status: codersdk.WorkspaceAgentDisconnected, - }, + agent := codersdk.WorkspaceAgent{ + Status: codersdk.WorkspaceAgentDisconnected, } go func() { time.Sleep(3 * time.Second) - resource.Agent.Status = codersdk.WorkspaceAgentConnected + agent.Status = codersdk.WorkspaceAgentConnected }() err := cliui.Agent(cmd.Context(), cmd.OutOrStdout(), cliui.AgentOptions{ WorkspaceName: "dev", - Fetch: func(ctx context.Context) (codersdk.WorkspaceResource, error) { - return resource, nil + Fetch: func(ctx context.Context) (codersdk.WorkspaceAgent, error) { + return agent, nil }, WarnInterval: 2 * time.Second, }) diff --git a/cmd/templater/main.go b/cmd/templater/main.go index 825ab6a3f0..effd36fd49 100644 --- a/cmd/templater/main.go +++ b/cmd/templater/main.go @@ -195,12 +195,11 @@ func parse(cmd *cobra.Command, parameters []codersdk.CreateParameterRequest) err return err } for _, resource := range resources { - if resource.Agent == nil { - continue - } - err = awaitAgent(cmd.Context(), client, resource) - if err != nil { - return err + for _, agent := range resource.Agents { + err = awaitAgent(cmd.Context(), client, agent) + if err != nil { + return err + } } } @@ -229,7 +228,7 @@ func parse(cmd *cobra.Command, parameters []codersdk.CreateParameterRequest) err return nil } -func awaitAgent(ctx context.Context, client *codersdk.Client, resource codersdk.WorkspaceResource) error { +func awaitAgent(ctx context.Context, client *codersdk.Client, agent codersdk.WorkspaceAgent) error { ticker := time.NewTicker(time.Second) defer ticker.Stop() for { @@ -237,11 +236,11 @@ func awaitAgent(ctx context.Context, client *codersdk.Client, resource codersdk. case <-ctx.Done(): return ctx.Err() case <-ticker.C: - resource, err := client.WorkspaceResource(ctx, resource.ID) + agent, err := client.WorkspaceAgent(ctx, agent.ID) if err != nil { return err } - if resource.Agent.FirstConnectedAt == nil { + if agent.FirstConnectedAt == nil { continue } return nil diff --git a/coderd/coderd.go b/coderd/coderd.go index b82f2eef9b..3a6e3c2d93 100644 --- a/coderd/coderd.go +++ b/coderd/coderd.go @@ -168,26 +168,31 @@ func New(options *Options) (http.Handler, func()) { }) }) }) - r.Route("/workspaceresources", func(r chi.Router) { - r.Route("/auth", func(r chi.Router) { - r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity) - r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) - }) - r.Route("/agent", func(r chi.Router) { + r.Route("/workspaceagents", func(r chi.Router) { + r.Post("/aws-instance-identity", api.postWorkspaceAuthAWSInstanceIdentity) + r.Post("/google-instance-identity", api.postWorkspaceAuthGoogleInstanceIdentity) + r.Route("/me", func(r chi.Router) { r.Use(httpmw.ExtractWorkspaceAgent(options.Database)) r.Get("/", api.workspaceAgentListen) r.Get("/gitsshkey", api.agentGitSSHKey) }) - r.Route("/{workspaceresource}", func(r chi.Router) { + r.Route("/{workspaceagent}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), - httpmw.ExtractWorkspaceResourceParam(options.Database), - httpmw.ExtractWorkspaceParam(options.Database), + httpmw.ExtractWorkspaceAgentParam(options.Database), ) - r.Get("/", api.workspaceResource) - r.Get("/dial", api.workspaceResourceDial) + r.Get("/", api.workspaceAgent) + r.Get("/dial", api.workspaceAgentDial) }) }) + r.Route("/workspaceresources/{workspaceresource}", func(r chi.Router) { + r.Use( + httpmw.ExtractAPIKey(options.Database, nil), + httpmw.ExtractWorkspaceResourceParam(options.Database), + httpmw.ExtractWorkspaceParam(options.Database), + ) + r.Get("/", api.workspaceResource) + }) r.Route("/workspaces/{workspace}", func(r chi.Router) { r.Use( httpmw.ExtractAPIKey(options.Database, nil), diff --git a/coderd/coderdtest/coderdtest.go b/coderd/coderdtest/coderdtest.go index d56e900221..04667ef3f7 100644 --- a/coderd/coderdtest/coderdtest.go +++ b/coderd/coderdtest/coderdtest.go @@ -263,11 +263,10 @@ func AwaitWorkspaceAgents(t *testing.T, client *codersdk.Client, build uuid.UUID resources, err = client.WorkspaceResourcesByBuild(context.Background(), build) require.NoError(t, err) for _, resource := range resources { - if resource.Agent == nil { - continue - } - if resource.Agent.FirstConnectedAt == nil { - return false + for _, agent := range resource.Agents { + if agent.FirstConnectedAt == nil { + return false + } } } return true diff --git a/coderd/database/databasefake/databasefake.go b/coderd/database/databasefake/databasefake.go index 6ec7316891..45f652d038 100644 --- a/coderd/database/databasefake/databasefake.go +++ b/coderd/database/databasefake/databasefake.go @@ -634,6 +634,20 @@ func (q *fakeQuerier) GetWorkspaceAgentByAuthToken(_ context.Context, authToken return database.WorkspaceAgent{}, sql.ErrNoRows } +func (q *fakeQuerier) GetWorkspaceAgentByID(_ context.Context, id uuid.UUID) (database.WorkspaceAgent, error) { + q.mutex.RLock() + defer q.mutex.RUnlock() + + // The schema sorts this by created at, so we iterate the array backwards. + for i := len(q.provisionerJobAgent) - 1; i >= 0; i-- { + agent := q.provisionerJobAgent[i] + if agent.ID.String() == id.String() { + return agent, nil + } + } + return database.WorkspaceAgent{}, sql.ErrNoRows +} + func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceID string) (database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() @@ -648,16 +662,23 @@ func (q *fakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI return database.WorkspaceAgent{}, sql.ErrNoRows } -func (q *fakeQuerier) GetWorkspaceAgentByResourceID(_ context.Context, resourceID uuid.UUID) (database.WorkspaceAgent, error) { +func (q *fakeQuerier) GetWorkspaceAgentsByResourceIDs(_ context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() + workspaceAgents := make([]database.WorkspaceAgent, 0) for _, agent := range q.provisionerJobAgent { - if agent.ResourceID.String() == resourceID.String() { - return agent, nil + for _, resourceID := range resourceIDs { + if agent.ResourceID.String() != resourceID.String() { + continue + } + workspaceAgents = append(workspaceAgents, agent) } } - return database.WorkspaceAgent{}, sql.ErrNoRows + if len(workspaceAgents) == 0 { + return nil, sql.ErrNoRows + } + return workspaceAgents, nil } func (q *fakeQuerier) GetProvisionerDaemonByID(_ context.Context, id uuid.UUID) (database.ProvisionerDaemon, error) { @@ -982,6 +1003,9 @@ func (q *fakeQuerier) InsertWorkspaceAgent(_ context.Context, arg database.Inser AuthToken: arg.AuthToken, AuthInstanceID: arg.AuthInstanceID, EnvironmentVariables: arg.EnvironmentVariables, + Name: arg.Name, + Architecture: arg.Architecture, + OperatingSystem: arg.OperatingSystem, StartupScript: arg.StartupScript, InstanceMetadata: arg.InstanceMetadata, ResourceMetadata: arg.ResourceMetadata, @@ -1003,7 +1027,6 @@ func (q *fakeQuerier) InsertWorkspaceResource(_ context.Context, arg database.In Address: arg.Address, Type: arg.Type, Name: arg.Name, - AgentID: arg.AgentID, } q.provisionerJobResource = append(q.provisionerJobResource, resource) return resource, nil diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 1d6cab77e0..fe6b026abf 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -235,13 +235,16 @@ CREATE TABLE workspace_agents ( id uuid NOT NULL, created_at timestamp with time zone NOT NULL, updated_at timestamp with time zone NOT NULL, + name character varying(64) NOT NULL, first_connected_at timestamp with time zone, last_connected_at timestamp with time zone, disconnected_at timestamp with time zone, resource_id uuid NOT NULL, auth_token uuid NOT NULL, auth_instance_id character varying(64), + architecture character varying(64) NOT NULL, environment_variables jsonb, + operating_system character varying(64) NOT NULL, startup_script character varying(65534), instance_metadata jsonb, resource_metadata jsonb @@ -269,8 +272,7 @@ CREATE TABLE workspace_resources ( transition workspace_transition NOT NULL, address character varying(256) NOT NULL, type character varying(192) NOT NULL, - name character varying(64) NOT NULL, - agent_id uuid + name character varying(64) NOT NULL ); CREATE TABLE workspaces ( diff --git a/coderd/database/migrations/000004_jobs.up.sql b/coderd/database/migrations/000004_jobs.up.sql index dbdd6ce112..379857ed3a 100644 --- a/coderd/database/migrations/000004_jobs.up.sql +++ b/coderd/database/migrations/000004_jobs.up.sql @@ -69,7 +69,6 @@ CREATE TABLE workspace_resources ( address varchar(256) NOT NULL, type varchar(192) NOT NULL, name varchar(64) NOT NULL, - agent_id uuid, PRIMARY KEY (id) ); @@ -77,13 +76,16 @@ CREATE TABLE workspace_agents ( id uuid NOT NULL, created_at timestamptz NOT NULL, updated_at timestamptz NOT NULL, + name varchar(64) NOT NULL, first_connected_at timestamptz, last_connected_at timestamptz, disconnected_at timestamptz, resource_id uuid NOT NULL REFERENCES workspace_resources (id) ON DELETE CASCADE, auth_token uuid NOT NULL UNIQUE, auth_instance_id varchar(64), + architecture varchar(64) NOT NULL, environment_variables jsonb, + operating_system varchar(64) NOT NULL, startup_script varchar(65534), instance_metadata jsonb, resource_metadata jsonb, diff --git a/coderd/database/models.go b/coderd/database/models.go index f8ab1959a0..55dc014d82 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -403,13 +403,16 @@ type WorkspaceAgent struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` FirstConnectedAt sql.NullTime `db:"first_connected_at" json:"first_connected_at"` LastConnectedAt sql.NullTime `db:"last_connected_at" json:"last_connected_at"` DisconnectedAt sql.NullTime `db:"disconnected_at" json:"disconnected_at"` ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` + Architecture string `db:"architecture" json:"architecture"` EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` + OperatingSystem string `db:"operating_system" json:"operating_system"` StartupScript sql.NullString `db:"startup_script" json:"startup_script"` InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` @@ -438,5 +441,4 @@ type WorkspaceResource struct { Address string `db:"address" json:"address"` Type string `db:"type" json:"type"` Name string `db:"name" json:"name"` - AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` } diff --git a/coderd/database/querier.go b/coderd/database/querier.go index 9da31284a5..120da839a1 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -39,8 +39,9 @@ type querier interface { GetUserByID(ctx context.Context, id uuid.UUID) (User, error) GetUserCount(ctx context.Context) (int64, error) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (WorkspaceAgent, error) - GetWorkspaceAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (WorkspaceAgent, error) + GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) GetWorkspaceBuildByID(ctx context.Context, id uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByJobID(ctx context.Context, jobID uuid.UUID) (WorkspaceBuild, error) GetWorkspaceBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceBuild, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f0d620d2e6..b37c1e53c7 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -1907,7 +1907,7 @@ func (q *sqlQuerier) InsertUser(ctx context.Context, arg InsertUserParams) (User const getWorkspaceAgentByAuthToken = `-- name: GetWorkspaceAgentByAuthToken :one SELECT - id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata FROM workspace_agents WHERE @@ -1923,13 +1923,49 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.Name, &i.FirstConnectedAt, &i.LastConnectedAt, &i.DisconnectedAt, &i.ResourceID, &i.AuthToken, &i.AuthInstanceID, + &i.Architecture, &i.EnvironmentVariables, + &i.OperatingSystem, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ) + return i, err +} + +const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one +SELECT + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata +FROM + workspace_agents +WHERE + id = $1 +` + +func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) { + row := q.db.QueryRowContext(ctx, getWorkspaceAgentByID, id) + var i WorkspaceAgent + err := row.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.FirstConnectedAt, + &i.LastConnectedAt, + &i.DisconnectedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.Architecture, + &i.EnvironmentVariables, + &i.OperatingSystem, &i.StartupScript, &i.InstanceMetadata, &i.ResourceMetadata, @@ -1939,7 +1975,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByAuthToken(ctx context.Context, authToken const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata FROM workspace_agents WHERE @@ -1955,13 +1991,16 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.Name, &i.FirstConnectedAt, &i.LastConnectedAt, &i.DisconnectedAt, &i.ResourceID, &i.AuthToken, &i.AuthInstanceID, + &i.Architecture, &i.EnvironmentVariables, + &i.OperatingSystem, &i.StartupScript, &i.InstanceMetadata, &i.ResourceMetadata, @@ -1969,34 +2008,53 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst return i, err } -const getWorkspaceAgentByResourceID = `-- name: GetWorkspaceAgentByResourceID :one +const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata FROM workspace_agents WHERE - resource_id = $1 + resource_id = ANY($1 :: uuid [ ]) ` -func (q *sqlQuerier) GetWorkspaceAgentByResourceID(ctx context.Context, resourceID uuid.UUID) (WorkspaceAgent, error) { - row := q.db.QueryRowContext(ctx, getWorkspaceAgentByResourceID, resourceID) - var i WorkspaceAgent - err := row.Scan( - &i.ID, - &i.CreatedAt, - &i.UpdatedAt, - &i.FirstConnectedAt, - &i.LastConnectedAt, - &i.DisconnectedAt, - &i.ResourceID, - &i.AuthToken, - &i.AuthInstanceID, - &i.EnvironmentVariables, - &i.StartupScript, - &i.InstanceMetadata, - &i.ResourceMetadata, - ) - return i, err +func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) { + rows, err := q.db.QueryContext(ctx, getWorkspaceAgentsByResourceIDs, pq.Array(ids)) + if err != nil { + return nil, err + } + defer rows.Close() + var items []WorkspaceAgent + for rows.Next() { + var i WorkspaceAgent + if err := rows.Scan( + &i.ID, + &i.CreatedAt, + &i.UpdatedAt, + &i.Name, + &i.FirstConnectedAt, + &i.LastConnectedAt, + &i.DisconnectedAt, + &i.ResourceID, + &i.AuthToken, + &i.AuthInstanceID, + &i.Architecture, + &i.EnvironmentVariables, + &i.OperatingSystem, + &i.StartupScript, + &i.InstanceMetadata, + &i.ResourceMetadata, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil } const insertWorkspaceAgent = `-- name: InsertWorkspaceAgent :one @@ -2005,26 +2063,32 @@ INSERT INTO id, created_at, updated_at, + name, resource_id, auth_token, auth_instance_id, + architecture, environment_variables, + operating_system, startup_script, instance_metadata, resource_metadata ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id, created_at, updated_at, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, environment_variables, startup_script, instance_metadata, resource_metadata + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, startup_script, instance_metadata, resource_metadata ` type InsertWorkspaceAgentParams struct { ID uuid.UUID `db:"id" json:"id"` CreatedAt time.Time `db:"created_at" json:"created_at"` UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + Name string `db:"name" json:"name"` ResourceID uuid.UUID `db:"resource_id" json:"resource_id"` AuthToken uuid.UUID `db:"auth_token" json:"auth_token"` AuthInstanceID sql.NullString `db:"auth_instance_id" json:"auth_instance_id"` + Architecture string `db:"architecture" json:"architecture"` EnvironmentVariables pqtype.NullRawMessage `db:"environment_variables" json:"environment_variables"` + OperatingSystem string `db:"operating_system" json:"operating_system"` StartupScript sql.NullString `db:"startup_script" json:"startup_script"` InstanceMetadata pqtype.NullRawMessage `db:"instance_metadata" json:"instance_metadata"` ResourceMetadata pqtype.NullRawMessage `db:"resource_metadata" json:"resource_metadata"` @@ -2035,10 +2099,13 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa arg.ID, arg.CreatedAt, arg.UpdatedAt, + arg.Name, arg.ResourceID, arg.AuthToken, arg.AuthInstanceID, + arg.Architecture, arg.EnvironmentVariables, + arg.OperatingSystem, arg.StartupScript, arg.InstanceMetadata, arg.ResourceMetadata, @@ -2048,13 +2115,16 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa &i.ID, &i.CreatedAt, &i.UpdatedAt, + &i.Name, &i.FirstConnectedAt, &i.LastConnectedAt, &i.DisconnectedAt, &i.ResourceID, &i.AuthToken, &i.AuthInstanceID, + &i.Architecture, &i.EnvironmentVariables, + &i.OperatingSystem, &i.StartupScript, &i.InstanceMetadata, &i.ResourceMetadata, @@ -2405,7 +2475,7 @@ func (q *sqlQuerier) UpdateWorkspaceBuildByID(ctx context.Context, arg UpdateWor const getWorkspaceResourceByID = `-- name: GetWorkspaceResourceByID :one SELECT - id, created_at, job_id, transition, address, type, name, agent_id + id, created_at, job_id, transition, address, type, name FROM workspace_resources WHERE @@ -2423,14 +2493,13 @@ func (q *sqlQuerier) GetWorkspaceResourceByID(ctx context.Context, id uuid.UUID) &i.Address, &i.Type, &i.Name, - &i.AgentID, ) return i, err } const getWorkspaceResourcesByJobID = `-- name: GetWorkspaceResourcesByJobID :many SELECT - id, created_at, job_id, transition, address, type, name, agent_id + id, created_at, job_id, transition, address, type, name FROM workspace_resources WHERE @@ -2454,7 +2523,6 @@ func (q *sqlQuerier) GetWorkspaceResourcesByJobID(ctx context.Context, jobID uui &i.Address, &i.Type, &i.Name, - &i.AgentID, ); err != nil { return nil, err } @@ -2478,11 +2546,10 @@ INSERT INTO transition, address, type, - name, - agent_id + name ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id, created_at, job_id, transition, address, type, name, agent_id + ($1, $2, $3, $4, $5, $6, $7) RETURNING id, created_at, job_id, transition, address, type, name ` type InsertWorkspaceResourceParams struct { @@ -2493,7 +2560,6 @@ type InsertWorkspaceResourceParams struct { Address string `db:"address" json:"address"` Type string `db:"type" json:"type"` Name string `db:"name" json:"name"` - AgentID uuid.NullUUID `db:"agent_id" json:"agent_id"` } func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWorkspaceResourceParams) (WorkspaceResource, error) { @@ -2505,7 +2571,6 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork arg.Address, arg.Type, arg.Name, - arg.AgentID, ) var i WorkspaceResource err := row.Scan( @@ -2516,7 +2581,6 @@ func (q *sqlQuerier) InsertWorkspaceResource(ctx context.Context, arg InsertWork &i.Address, &i.Type, &i.Name, - &i.AgentID, ) return i, err } diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index a2a206bba0..7d9da4b64a 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -8,6 +8,14 @@ WHERE ORDER BY created_at DESC; +-- name: GetWorkspaceAgentByID :one +SELECT + * +FROM + workspace_agents +WHERE + id = $1; + -- name: GetWorkspaceAgentByInstanceID :one SELECT * @@ -18,13 +26,13 @@ WHERE ORDER BY created_at DESC; --- name: GetWorkspaceAgentByResourceID :one +-- name: GetWorkspaceAgentsByResourceIDs :many SELECT * FROM workspace_agents WHERE - resource_id = $1; + resource_id = ANY(@ids :: uuid [ ]); -- name: InsertWorkspaceAgent :one INSERT INTO @@ -32,16 +40,19 @@ INSERT INTO id, created_at, updated_at, + name, resource_id, auth_token, auth_instance_id, + architecture, environment_variables, + operating_system, startup_script, instance_metadata, resource_metadata ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13) RETURNING *; -- name: UpdateWorkspaceAgentConnectionByID :exec UPDATE diff --git a/coderd/database/queries/workspaceresources.sql b/coderd/database/queries/workspaceresources.sql index 17a352a67f..869f4a6311 100644 --- a/coderd/database/queries/workspaceresources.sql +++ b/coderd/database/queries/workspaceresources.sql @@ -23,8 +23,7 @@ INSERT INTO transition, address, type, - name, - agent_id + name ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING *; + ($1, $2, $3, $4, $5, $6, $7) RETURNING *; diff --git a/coderd/gitsshkey_test.go b/coderd/gitsshkey_test.go index affb5d0184..dc9ffe23ca 100644 --- a/coderd/gitsshkey_test.go +++ b/coderd/gitsshkey_test.go @@ -79,51 +79,40 @@ func TestGitSSHKey(t *testing.T) { func TestAgentGitSSHKey(t *testing.T) { t.Parallel() - agentClient := func(algo gitsshkey.Algorithm) *codersdk.Client { - client := coderdtest.New(t, &coderdtest.Options{ - SSHKeygenAlgorithm: algo, - }) - user := coderdtest.CreateFirstUser(t, client) - daemonCloser := coderdtest.NewProvisionerDaemon(t, client) - authToken := uuid.NewString() - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionDryRun: echo.ProvisionComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agent: &proto.Agent{ - Id: uuid.NewString(), - Auth: &proto.Agent_Token{ - Token: authToken, - }, + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, }, }}, - }, + }}, }, - }}, - }) - project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) - coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - daemonCloser.Close() - - agentClient := codersdk.New(client.URL) - agentClient.SessionToken = authToken - - return agentClient - } - - t.Run("AgentKey", func(t *testing.T) { - t.Parallel() - ctx := context.Background() - client := agentClient(gitsshkey.AlgorithmEd25519) - agentKey, err := client.AgentGitSSHKey(ctx) - require.NoError(t, err) - require.NotEmpty(t, agentKey.PrivateKey) + }, + }}, }) + project := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + daemonCloser.Close() + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + + agentKey, err := agentClient.AgentGitSSHKey(context.Background()) + require.NoError(t, err) + require.NotEmpty(t, agentKey.PrivateKey) } diff --git a/coderd/httpmw/workspaceagentparam.go b/coderd/httpmw/workspaceagentparam.go new file mode 100644 index 0000000000..de2b499e9b --- /dev/null +++ b/coderd/httpmw/workspaceagentparam.go @@ -0,0 +1,94 @@ +package httpmw + +import ( + "context" + "database/sql" + "errors" + "fmt" + "net/http" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpapi" +) + +type workspaceAgentParamContextKey struct{} + +// WorkspaceAgentParam returns the workspace agent from the ExtractWorkspaceAgentParam handler. +func WorkspaceAgentParam(r *http.Request) database.WorkspaceAgent { + user, ok := r.Context().Value(workspaceAgentParamContextKey{}).(database.WorkspaceAgent) + if !ok { + panic("developer error: agent middleware not provided") + } + return user +} + +// ExtractWorkspaceAgentParam grabs a workspace agent from the "workspaceagent" URL parameter. +func ExtractWorkspaceAgentParam(db database.Store) func(http.Handler) http.Handler { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { + agentUUID, parsed := parseUUID(rw, r, "workspaceagent") + if !parsed { + return + } + agent, err := db.GetWorkspaceAgentByID(r.Context(), agentUUID) + if errors.Is(err, sql.ErrNoRows) { + httpapi.Write(rw, http.StatusNotFound, httpapi.Response{ + Message: "agent doesn't exist with that id", + }) + return + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get agent: %s", err), + }) + return + } + resource, err := db.GetWorkspaceResourceByID(r.Context(), agent.ResourceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get resource: %s", err), + }) + return + } + + job, err := db.GetProvisionerJobByID(r.Context(), resource.JobID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get job: %s", err), + }) + return + } + if job.Type != database.ProvisionerJobTypeWorkspaceBuild { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: "Workspace agents can only be fetched for builds.", + }) + return + } + build, err := db.GetWorkspaceBuildByJobID(r.Context(), job.ID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace build: %s", err), + }) + return + } + workspace, err := db.GetWorkspaceByID(r.Context(), build.WorkspaceID) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace: %s", err), + }) + return + } + + apiKey := APIKey(r) + if apiKey.UserID != workspace.OwnerID { + httpapi.Write(rw, http.StatusUnauthorized, httpapi.Response{ + Message: "getting non-personal agents isn't supported", + }) + return + } + + ctx := context.WithValue(r.Context(), workspaceAgentParamContextKey{}, agent) + next.ServeHTTP(rw, r.WithContext(ctx)) + }) + } +} diff --git a/coderd/httpmw/workspaceagentparam_test.go b/coderd/httpmw/workspaceagentparam_test.go new file mode 100644 index 0000000000..f014a8bd55 --- /dev/null +++ b/coderd/httpmw/workspaceagentparam_test.go @@ -0,0 +1,153 @@ +package httpmw_test + +import ( + "context" + "crypto/sha256" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/database/databasefake" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/cryptorand" +) + +func TestWorkspaceAgentParam(t *testing.T) { + t.Parallel() + + setupAuthentication := func(db database.Store) (*http.Request, database.WorkspaceAgent) { + var ( + id, secret = randomAPIKeyParts() + hashed = sha256.Sum256([]byte(secret)) + ) + r := httptest.NewRequest("GET", "/", nil) + r.AddCookie(&http.Cookie{ + Name: httpmw.AuthCookie, + Value: fmt.Sprintf("%s-%s", id, secret), + }) + + userID := uuid.New() + username, err := cryptorand.String(8) + require.NoError(t, err) + user, err := db.InsertUser(r.Context(), database.InsertUserParams{ + ID: userID, + Email: "testaccount@coder.com", + Name: "example", + LoginType: database.LoginTypeBuiltIn, + HashedPassword: hashed[:], + Username: username, + CreatedAt: database.Now(), + UpdatedAt: database.Now(), + }) + require.NoError(t, err) + + _, err = db.InsertAPIKey(r.Context(), database.InsertAPIKeyParams{ + ID: id, + UserID: user.ID, + HashedSecret: hashed[:], + LastUsed: database.Now(), + ExpiresAt: database.Now().Add(time.Minute), + }) + require.NoError(t, err) + + workspace, err := db.InsertWorkspace(context.Background(), database.InsertWorkspaceParams{ + ID: uuid.New(), + TemplateID: uuid.New(), + OwnerID: user.ID, + Name: "potato", + }) + require.NoError(t, err) + + build, err := db.InsertWorkspaceBuild(context.Background(), database.InsertWorkspaceBuildParams{ + ID: uuid.New(), + WorkspaceID: workspace.ID, + JobID: uuid.New(), + }) + require.NoError(t, err) + + job, err := db.InsertProvisionerJob(context.Background(), database.InsertProvisionerJobParams{ + ID: build.JobID, + Type: database.ProvisionerJobTypeWorkspaceBuild, + }) + require.NoError(t, err) + + resource, err := db.InsertWorkspaceResource(context.Background(), database.InsertWorkspaceResourceParams{ + ID: uuid.New(), + JobID: job.ID, + }) + require.NoError(t, err) + + agent, err := db.InsertWorkspaceAgent(context.Background(), database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + ResourceID: resource.ID, + }) + require.NoError(t, err) + + ctx := chi.NewRouteContext() + ctx.URLParams.Add("user", userID.String()) + ctx.URLParams.Add("workspaceagent", agent.ID.String()) + r = r.WithContext(context.WithValue(r.Context(), chi.RouteCtxKey, ctx)) + return r, agent + } + + t.Run("None", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use(httpmw.ExtractWorkspaceBuildParam(db)) + rtr.Get("/", nil) + r, _ := setupAuthentication(db) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusBadRequest, res.StatusCode) + }) + + t.Run("NotFound", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use(httpmw.ExtractWorkspaceAgentParam(db)) + rtr.Get("/", nil) + + r, _ := setupAuthentication(db) + chi.RouteContext(r.Context()).URLParams.Add("workspaceagent", uuid.NewString()) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusNotFound, res.StatusCode) + }) + + t.Run("WorkspaceAgent", func(t *testing.T) { + t.Parallel() + db := databasefake.New() + rtr := chi.NewRouter() + rtr.Use( + httpmw.ExtractAPIKey(db, nil), + httpmw.ExtractWorkspaceAgentParam(db), + ) + rtr.Get("/", func(rw http.ResponseWriter, r *http.Request) { + _ = httpmw.WorkspaceAgentParam(r) + rw.WriteHeader(http.StatusOK) + }) + + r, _ := setupAuthentication(db) + rw := httptest.NewRecorder() + rtr.ServeHTTP(rw, r) + + res := rw.Result() + defer res.Body.Close() + require.Equal(t, http.StatusOK, res.StatusCode) + }) +} diff --git a/coderd/provisionerdaemons.go b/coderd/provisionerdaemons.go index 1de20ce4c1..4499bbbc3d 100644 --- a/coderd/provisionerdaemons.go +++ b/coderd/provisionerdaemons.go @@ -587,25 +587,21 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. Address: address, Type: protoResource.Type, Name: protoResource.Name, - AgentID: uuid.NullUUID{ - UUID: uuid.New(), - Valid: protoResource.Agent != nil, - }, }) if err != nil { return xerrors.Errorf("insert provisioner job resource %q: %w", protoResource.Name, err) } - if resource.AgentID.Valid { + for _, agent := range protoResource.Agents { var instanceID sql.NullString - if protoResource.Agent.GetInstanceId() != "" { + if agent.GetInstanceId() != "" { instanceID = sql.NullString{ - String: protoResource.Agent.GetInstanceId(), + String: agent.GetInstanceId(), Valid: true, } } var env pqtype.NullRawMessage - if protoResource.Agent.Env != nil { - data, err := json.Marshal(protoResource.Agent.Env) + if agent.Env != nil { + data, err := json.Marshal(agent.Env) if err != nil { return xerrors.Errorf("marshal env: %w", err) } @@ -615,24 +611,27 @@ func insertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. } } authToken := uuid.New() - if protoResource.Agent.GetToken() != "" { - authToken, err = uuid.Parse(protoResource.Agent.GetToken()) + if agent.GetToken() != "" { + authToken, err = uuid.Parse(agent.GetToken()) if err != nil { return xerrors.Errorf("invalid auth token format; must be uuid: %w", err) } } _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ - ID: resource.AgentID.UUID, + ID: uuid.New(), CreatedAt: database.Now(), UpdatedAt: database.Now(), ResourceID: resource.ID, + Name: agent.Name, AuthToken: authToken, AuthInstanceID: instanceID, + Architecture: agent.Architecture, EnvironmentVariables: env, + OperatingSystem: agent.OperatingSystem, StartupScript: sql.NullString{ - String: protoResource.Agent.StartupScript, - Valid: protoResource.Agent.StartupScript != "", + String: agent.StartupScript, + Valid: agent.StartupScript != "", }, }) if err != nil { diff --git a/coderd/provisionerjobs.go b/coderd/provisionerjobs.go index 349805fafb..71a8c0bdd2 100644 --- a/coderd/provisionerjobs.go +++ b/coderd/provisionerjobs.go @@ -196,27 +196,38 @@ func (api *api) provisionerJobResources(rw http.ResponseWriter, r *http.Request, }) return } + resourceIDs := make([]uuid.UUID, 0) + for _, resource := range resources { + resourceIDs = append(resourceIDs, resource.ID) + } + resourceAgents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), resourceIDs) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get workspace agents by resources: %s", err), + }) + return + } + apiResources := make([]codersdk.WorkspaceResource, 0) for _, resource := range resources { - if !resource.AgentID.Valid { - apiResources = append(apiResources, convertWorkspaceResource(resource, nil)) - continue + agents := make([]codersdk.WorkspaceAgent, 0) + for _, agent := range resourceAgents { + if agent.ResourceID != resource.ID { + continue + } + apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert provisioner job agent: %s", err), + }) + return + } + agents = append(agents, apiAgent) } - agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job agent: %s", err), - }) - return - } - apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("convert provisioner job agent: %s", err), - }) - return - } - apiResources = append(apiResources, convertWorkspaceResource(resource, &apiAgent)) + apiResources = append(apiResources, convertWorkspaceResource(resource, agents)) } render.Status(r, http.StatusOK) render.JSON(rw, r, apiResources) diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index a0f41ccb21..d7e00cdd43 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -210,10 +210,10 @@ func TestTemplateVersionResources(t *testing.T) { Resources: []*proto.Resource{{ Name: "some", Type: "example", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - }, + }}, }, { Name: "another", Type: "example", @@ -229,7 +229,7 @@ func TestTemplateVersionResources(t *testing.T) { require.Len(t, resources, 4) require.Equal(t, "some", resources[0].Name) require.Equal(t, "example", resources[0].Type) - require.NotNil(t, resources[0].Agent) + require.Len(t, resources[0].Agents, 1) }) } @@ -255,12 +255,12 @@ func TestTemplateVersionLogs(t *testing.T) { Resources: []*proto.Resource{{ Name: "some", Type: "example", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{ Token: uuid.NewString(), }, - }, + }}, }, { Name: "another", Type: "example", diff --git a/coderd/workspaceagents.go b/coderd/workspaceagents.go new file mode 100644 index 0000000000..808ebfac0a --- /dev/null +++ b/coderd/workspaceagents.go @@ -0,0 +1,257 @@ +package coderd + +import ( + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/go-chi/render" + "github.com/hashicorp/yamux" + "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "cdr.dev/slog" + "github.com/coder/coder/coderd/database" + "github.com/coder/coder/coderd/httpapi" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" +) + +func (api *api) workspaceAgent(rw http.ResponseWriter, r *http.Request) { + agent := httpmw.WorkspaceAgentParam(r) + apiAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("convert workspace agent: %s", err), + }) + return + } + render.Status(r, http.StatusOK) + render.JSON(rw, r, apiAgent) +} + +func (api *api) workspaceAgentDial(rw http.ResponseWriter, r *http.Request) { + api.websocketWaitMutex.Lock() + api.websocketWaitGroup.Add(1) + api.websocketWaitMutex.Unlock() + defer api.websocketWaitGroup.Done() + + agent := httpmw.WorkspaceAgentParam(r) + conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), + }) + return + } + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, "") + }() + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + err = peerbroker.ProxyListen(r.Context(), session, peerbroker.ProxyOptions{ + ChannelID: agent.ID.String(), + Logger: api.Logger.Named("peerbroker-proxy-dial"), + Pubsub: api.Pubsub, + }) + if err != nil { + _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("serve: %s", err)) + return + } +} + +func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { + api.websocketWaitMutex.Lock() + api.websocketWaitGroup.Add(1) + api.websocketWaitMutex.Unlock() + defer api.websocketWaitGroup.Done() + + agent := httpmw.WorkspaceAgent(r) + conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), + }) + return + } + resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID) + if err != nil { + httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ + Message: fmt.Sprintf("accept websocket: %s", err), + }) + return + } + + defer func() { + _ = conn.Close(websocket.StatusNormalClosure, "") + }() + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + closer, err := peerbroker.ProxyDial(proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), peerbroker.ProxyOptions{ + ChannelID: agent.ID.String(), + Pubsub: api.Pubsub, + Logger: api.Logger.Named("peerbroker-proxy-listen"), + }) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + defer closer.Close() + firstConnectedAt := agent.FirstConnectedAt + if !firstConnectedAt.Valid { + firstConnectedAt = sql.NullTime{ + Time: database.Now(), + Valid: true, + } + } + lastConnectedAt := sql.NullTime{ + Time: database.Now(), + Valid: true, + } + disconnectedAt := agent.DisconnectedAt + updateConnectionTimes := func() error { + err = api.Database.UpdateWorkspaceAgentConnectionByID(r.Context(), database.UpdateWorkspaceAgentConnectionByIDParams{ + ID: agent.ID, + FirstConnectedAt: firstConnectedAt, + LastConnectedAt: lastConnectedAt, + DisconnectedAt: disconnectedAt, + }) + if err != nil { + return err + } + return nil + } + build, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID) + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + // Ensure the resource is still valid! + // We only accept agents for resources on the latest build. + ensureLatestBuild := func() error { + latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), build.WorkspaceID) + if err != nil { + return err + } + if build.ID.String() != latestBuild.ID.String() { + return xerrors.New("build is outdated") + } + return nil + } + + defer func() { + disconnectedAt = sql.NullTime{ + Time: database.Now(), + Valid: true, + } + _ = updateConnectionTimes() + }() + + err = updateConnectionTimes() + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + err = ensureLatestBuild() + if err != nil { + _ = conn.Close(websocket.StatusGoingAway, "") + return + } + + api.Logger.Info(r.Context(), "accepting agent", slog.F("resource", resource), slog.F("agent", agent)) + + ticker := time.NewTicker(api.AgentConnectionUpdateFrequency) + defer ticker.Stop() + for { + select { + case <-session.CloseChan(): + return + case <-ticker.C: + lastConnectedAt = sql.NullTime{ + Time: database.Now(), + Valid: true, + } + err = updateConnectionTimes() + if err != nil { + _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) + return + } + err = ensureLatestBuild() + if err != nil { + // Disconnect agents that are no longer valid. + _ = conn.Close(websocket.StatusGoingAway, "") + return + } + } + } +} + +func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { + var envs map[string]string + if dbAgent.EnvironmentVariables.Valid { + err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs) + if err != nil { + return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err) + } + } + agent := codersdk.WorkspaceAgent{ + ID: dbAgent.ID, + CreatedAt: dbAgent.CreatedAt, + UpdatedAt: dbAgent.UpdatedAt, + ResourceID: dbAgent.ResourceID, + InstanceID: dbAgent.AuthInstanceID.String, + Name: dbAgent.Name, + Architecture: dbAgent.Architecture, + OperatingSystem: dbAgent.OperatingSystem, + StartupScript: dbAgent.StartupScript.String, + EnvironmentVariables: envs, + } + if dbAgent.FirstConnectedAt.Valid { + agent.FirstConnectedAt = &dbAgent.FirstConnectedAt.Time + } + if dbAgent.LastConnectedAt.Valid { + agent.LastConnectedAt = &dbAgent.LastConnectedAt.Time + } + if dbAgent.DisconnectedAt.Valid { + agent.DisconnectedAt = &dbAgent.DisconnectedAt.Time + } + switch { + case !dbAgent.FirstConnectedAt.Valid: + // If the agent never connected, it's waiting for the compute + // to start up. + agent.Status = codersdk.WorkspaceAgentWaiting + case dbAgent.DisconnectedAt.Time.After(dbAgent.LastConnectedAt.Time): + // If we've disconnected after our last connection, we know the + // agent is no longer connected. + agent.Status = codersdk.WorkspaceAgentDisconnected + case agentUpdateFrequency*2 >= database.Now().Sub(dbAgent.LastConnectedAt.Time): + // The connection updated it's timestamp within the update frequency. + // We multiply by two to allow for some lag. + agent.Status = codersdk.WorkspaceAgentConnected + case database.Now().Sub(dbAgent.LastConnectedAt.Time) > agentUpdateFrequency*2: + // The connection died without updating the last connected. + agent.Status = codersdk.WorkspaceAgentDisconnected + } + + return agent, nil +} diff --git a/coderd/workspaceagents_test.go b/coderd/workspaceagents_test.go new file mode 100644 index 0000000000..8905b7277e --- /dev/null +++ b/coderd/workspaceagents_test.go @@ -0,0 +1,108 @@ +package coderd_test + +import ( + "context" + "testing" + + "github.com/google/uuid" + "github.com/stretchr/testify/require" + + "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/coderdtest" + "github.com/coder/coder/codersdk" + "github.com/coder/coder/peer" + "github.com/coder/coder/provisioner/echo" + "github.com/coder/coder/provisionersdk/proto" +) + +func TestWorkspaceAgent(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + daemonCloser.Close() + + resources, err := client.WorkspaceResourcesByBuild(context.Background(), workspace.LatestBuild.ID) + require.NoError(t, err) + _, err = client.WorkspaceAgent(context.Background(), resources[0].Agents[0].ID) + require.NoError(t, err) +} + +func TestWorkspaceAgentListen(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, nil) + user := coderdtest.CreateFirstUser(t, client) + daemonCloser := coderdtest.NewProvisionerDaemon(t, client) + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionDryRun: echo.ProvisionComplete, + Provision: []*proto.Provision_Response{{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "example", + Type: "aws_instance", + Agents: []*proto.Agent{{ + Id: uuid.NewString(), + Auth: &proto.Agent_Token{ + Token: authToken, + }, + }}, + }}, + }, + }, + }}, + }) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + coderdtest.AwaitTemplateVersionJob(t, client, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) + coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) + daemonCloser.Close() + + agentClient := codersdk.New(client.URL) + agentClient.SessionToken = authToken + agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil), + }) + t.Cleanup(func() { + _ = agentCloser.Close() + }) + resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) + conn, err := client.DialWorkspaceAgent(context.Background(), resources[0].Agents[0].ID, nil, &peer.ConnOptions{ + Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug), + }) + require.NoError(t, err) + t.Cleanup(func() { + _ = conn.Close() + }) + _, err = conn.Ping() + require.NoError(t, err) +} diff --git a/coderd/workspacebuilds.go b/coderd/workspacebuilds.go index 6ee8f2f44e..b0e21b5381 100644 --- a/coderd/workspacebuilds.go +++ b/coderd/workspacebuilds.go @@ -106,7 +106,7 @@ func convertWorkspaceBuild(workspaceBuild database.WorkspaceBuild, job codersdk. } } -func convertWorkspaceResource(resource database.WorkspaceResource, agent *codersdk.WorkspaceAgent) codersdk.WorkspaceResource { +func convertWorkspaceResource(resource database.WorkspaceResource, agents []codersdk.WorkspaceAgent) codersdk.WorkspaceResource { return codersdk.WorkspaceResource{ ID: resource.ID, CreatedAt: resource.CreatedAt, @@ -115,6 +115,6 @@ func convertWorkspaceResource(resource database.WorkspaceResource, agent *coders Address: resource.Address, Type: resource.Type, Name: resource.Name, - Agent: agent, + Agents: agents, } } diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index 8a576ff707..2e3b2eb7e8 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -91,10 +91,10 @@ func TestWorkspaceBuildResources(t *testing.T) { Resources: []*proto.Resource{{ Name: "some", Type: "example", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - }, + }}, }, { Name: "another", Type: "example", @@ -113,7 +113,7 @@ func TestWorkspaceBuildResources(t *testing.T) { require.Len(t, resources, 2) require.Equal(t, "some", resources[0].Name) require.Equal(t, "example", resources[0].Type) - require.NotNil(t, resources[0].Agent) + require.Len(t, resources[0].Agents, 1) }) } @@ -138,10 +138,10 @@ func TestWorkspaceBuildLogs(t *testing.T) { Resources: []*proto.Resource{{ Name: "some", Type: "example", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - }, + }}, }, { Name: "another", Type: "example", diff --git a/coderd/workspaceresourceauth_test.go b/coderd/workspaceresourceauth_test.go index 9cfa8e7707..3bd9dbd455 100644 --- a/coderd/workspaceresourceauth_test.go +++ b/coderd/workspaceresourceauth_test.go @@ -32,11 +32,11 @@ func TestPostWorkspaceAuthAWSInstanceIdentity(t *testing.T) { Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, - }, + }}, }}, }, }, @@ -98,11 +98,11 @@ func TestPostWorkspaceAuthGoogleInstanceIdentity(t *testing.T) { Resources: []*proto.Resource{{ Name: "somename", Type: "someinstance", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Auth: &proto.Agent_InstanceId{ InstanceId: instanceID, }, - }, + }}, }}, }, }, diff --git a/coderd/workspaceresources.go b/coderd/workspaceresources.go index 532fc64ea5..cd854c349f 100644 --- a/coderd/workspaceresources.go +++ b/coderd/workspaceresources.go @@ -2,26 +2,16 @@ package coderd import ( "database/sql" - "encoding/json" + "errors" "fmt" - "io" "net/http" - "time" "github.com/go-chi/render" - "github.com/hashicorp/yamux" - "golang.org/x/xerrors" - "nhooyr.io/websocket" + "github.com/google/uuid" - "cdr.dev/slog" - - "github.com/coder/coder/coderd/database" "github.com/coder/coder/coderd/httpapi" "github.com/coder/coder/coderd/httpmw" "github.com/coder/coder/codersdk" - "github.com/coder/coder/peerbroker" - "github.com/coder/coder/peerbroker/proto" - "github.com/coder/coder/provisionersdk" ) func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { @@ -40,15 +30,18 @@ func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { }) return } - var apiAgent *codersdk.WorkspaceAgent - if workspaceResource.AgentID.Valid { - agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), workspaceResource.ID) - if err != nil { - httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job agent: %s", err), - }) - return - } + agents, err := api.Database.GetWorkspaceAgentsByResourceIDs(r.Context(), []uuid.UUID{workspaceResource.ID}) + if errors.Is(err, sql.ErrNoRows) { + err = nil + } + if err != nil { + httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ + Message: fmt.Sprintf("get provisioner job agents: %s", err), + }) + return + } + apiAgents := make([]codersdk.WorkspaceAgent, 0) + for _, agent := range agents { convertedAgent, err := convertWorkspaceAgent(agent, api.AgentConnectionUpdateFrequency) if err != nil { httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{ @@ -56,239 +49,9 @@ func (api *api) workspaceResource(rw http.ResponseWriter, r *http.Request) { }) return } - apiAgent = &convertedAgent + apiAgents = append(apiAgents, convertedAgent) } render.Status(r, http.StatusOK) - render.JSON(rw, r, convertWorkspaceResource(workspaceResource, apiAgent)) -} - -func (api *api) workspaceResourceDial(rw http.ResponseWriter, r *http.Request) { - api.websocketWaitMutex.Lock() - api.websocketWaitGroup.Add(1) - api.websocketWaitMutex.Unlock() - defer api.websocketWaitGroup.Done() - - resource := httpmw.WorkspaceResourceParam(r) - if !resource.AgentID.Valid { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: "resource doesn't have an agent", - }) - return - } - agent, err := api.Database.GetWorkspaceAgentByResourceID(r.Context(), resource.ID) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("get provisioner job agent: %s", err), - }) - return - } - conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ - CompressionMode: websocket.CompressionDisabled, - }) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("accept websocket: %s", err), - }) - return - } - defer func() { - _ = conn.Close(websocket.StatusNormalClosure, "") - }() - config := yamux.DefaultConfig() - config.LogOutput = io.Discard - session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config) - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - err = peerbroker.ProxyListen(r.Context(), session, peerbroker.ProxyOptions{ - ChannelID: agent.ID.String(), - Logger: api.Logger.Named("peerbroker-proxy-dial"), - Pubsub: api.Pubsub, - }) - if err != nil { - _ = conn.Close(websocket.StatusInternalError, httpapi.WebsocketCloseSprintf("serve: %s", err)) - return - } -} - -func (api *api) workspaceAgentListen(rw http.ResponseWriter, r *http.Request) { - api.websocketWaitMutex.Lock() - api.websocketWaitGroup.Add(1) - api.websocketWaitMutex.Unlock() - defer api.websocketWaitGroup.Done() - - agent := httpmw.WorkspaceAgent(r) - conn, err := websocket.Accept(rw, r, &websocket.AcceptOptions{ - CompressionMode: websocket.CompressionDisabled, - }) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("accept websocket: %s", err), - }) - return - } - resource, err := api.Database.GetWorkspaceResourceByID(r.Context(), agent.ResourceID) - if err != nil { - httpapi.Write(rw, http.StatusBadRequest, httpapi.Response{ - Message: fmt.Sprintf("accept websocket: %s", err), - }) - return - } - - defer func() { - _ = conn.Close(websocket.StatusNormalClosure, "") - }() - config := yamux.DefaultConfig() - config.LogOutput = io.Discard - session, err := yamux.Server(websocket.NetConn(r.Context(), conn, websocket.MessageBinary), config) - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - closer, err := peerbroker.ProxyDial(proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)), peerbroker.ProxyOptions{ - ChannelID: agent.ID.String(), - Pubsub: api.Pubsub, - Logger: api.Logger.Named("peerbroker-proxy-listen"), - }) - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - defer closer.Close() - firstConnectedAt := agent.FirstConnectedAt - if !firstConnectedAt.Valid { - firstConnectedAt = sql.NullTime{ - Time: database.Now(), - Valid: true, - } - } - lastConnectedAt := sql.NullTime{ - Time: database.Now(), - Valid: true, - } - disconnectedAt := agent.DisconnectedAt - updateConnectionTimes := func() error { - err = api.Database.UpdateWorkspaceAgentConnectionByID(r.Context(), database.UpdateWorkspaceAgentConnectionByIDParams{ - ID: agent.ID, - FirstConnectedAt: firstConnectedAt, - LastConnectedAt: lastConnectedAt, - DisconnectedAt: disconnectedAt, - }) - if err != nil { - return err - } - return nil - } - build, err := api.Database.GetWorkspaceBuildByJobID(r.Context(), resource.JobID) - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - // Ensure the resource is still valid! - // We only accept agents for resources on the latest build. - ensureLatestBuild := func() error { - latestBuild, err := api.Database.GetWorkspaceBuildByWorkspaceIDWithoutAfter(r.Context(), build.WorkspaceID) - if err != nil { - return err - } - if build.ID.String() != latestBuild.ID.String() { - return xerrors.New("build is outdated") - } - return nil - } - - defer func() { - disconnectedAt = sql.NullTime{ - Time: database.Now(), - Valid: true, - } - _ = updateConnectionTimes() - }() - - err = updateConnectionTimes() - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - err = ensureLatestBuild() - if err != nil { - _ = conn.Close(websocket.StatusGoingAway, "") - return - } - - api.Logger.Info(r.Context(), "accepting agent", slog.F("resource", resource), slog.F("agent", agent)) - - ticker := time.NewTicker(api.AgentConnectionUpdateFrequency) - defer ticker.Stop() - for { - select { - case <-session.CloseChan(): - return - case <-ticker.C: - lastConnectedAt = sql.NullTime{ - Time: database.Now(), - Valid: true, - } - err = updateConnectionTimes() - if err != nil { - _ = conn.Close(websocket.StatusAbnormalClosure, err.Error()) - return - } - err = ensureLatestBuild() - if err != nil { - // Disconnect agents that are no longer valid. - _ = conn.Close(websocket.StatusGoingAway, "") - return - } - } - } -} - -func convertWorkspaceAgent(dbAgent database.WorkspaceAgent, agentUpdateFrequency time.Duration) (codersdk.WorkspaceAgent, error) { - var envs map[string]string - if dbAgent.EnvironmentVariables.Valid { - err := json.Unmarshal(dbAgent.EnvironmentVariables.RawMessage, &envs) - if err != nil { - return codersdk.WorkspaceAgent{}, xerrors.Errorf("unmarshal: %w", err) - } - } - agent := codersdk.WorkspaceAgent{ - ID: dbAgent.ID, - CreatedAt: dbAgent.CreatedAt, - UpdatedAt: dbAgent.UpdatedAt, - ResourceID: dbAgent.ResourceID, - InstanceID: dbAgent.AuthInstanceID.String, - StartupScript: dbAgent.StartupScript.String, - EnvironmentVariables: envs, - } - if dbAgent.FirstConnectedAt.Valid { - agent.FirstConnectedAt = &dbAgent.FirstConnectedAt.Time - } - if dbAgent.LastConnectedAt.Valid { - agent.LastConnectedAt = &dbAgent.LastConnectedAt.Time - } - if dbAgent.DisconnectedAt.Valid { - agent.DisconnectedAt = &dbAgent.DisconnectedAt.Time - } - switch { - case !dbAgent.FirstConnectedAt.Valid: - // If the agent never connected, it's waiting for the compute - // to start up. - agent.Status = codersdk.WorkspaceAgentWaiting - case dbAgent.DisconnectedAt.Time.After(dbAgent.LastConnectedAt.Time): - // If we've disconnected after our last connection, we know the - // agent is no longer connected. - agent.Status = codersdk.WorkspaceAgentDisconnected - case agentUpdateFrequency*2 >= database.Now().Sub(dbAgent.LastConnectedAt.Time): - // The connection updated it's timestamp within the update frequency. - // We multiply by two to allow for some lag. - agent.Status = codersdk.WorkspaceAgentConnected - case database.Now().Sub(dbAgent.LastConnectedAt.Time) > agentUpdateFrequency*2: - // The connection died without updating the last connected. - agent.Status = codersdk.WorkspaceAgentDisconnected - } - - return agent, nil + render.JSON(rw, r, convertWorkspaceResource(workspaceResource, apiAgents)) } diff --git a/coderd/workspaceresources_test.go b/coderd/workspaceresources_test.go index df4e5be315..e5673c191c 100644 --- a/coderd/workspaceresources_test.go +++ b/coderd/workspaceresources_test.go @@ -4,16 +4,10 @@ import ( "context" "testing" - "github.com/google/uuid" "github.com/stretchr/testify/require" - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - - "github.com/coder/coder/agent" "github.com/coder/coder/coderd/coderdtest" "github.com/coder/coder/codersdk" - "github.com/coder/coder/peer" "github.com/coder/coder/provisioner/echo" "github.com/coder/coder/provisionersdk/proto" ) @@ -33,10 +27,10 @@ func TestWorkspaceResource(t *testing.T) { Resources: []*proto.Resource{{ Name: "some", Type: "example", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ Id: "something", Auth: &proto.Agent_Token{}, - }, + }}, }}, }, }, @@ -52,55 +46,3 @@ func TestWorkspaceResource(t *testing.T) { require.NoError(t, err) }) } - -func TestWorkspaceAgentListen(t *testing.T) { - t.Parallel() - client := coderdtest.New(t, nil) - user := coderdtest.CreateFirstUser(t, client) - daemonCloser := coderdtest.NewProvisionerDaemon(t, client) - authToken := uuid.NewString() - version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ - Parse: echo.ParseComplete, - ProvisionDryRun: echo.ProvisionComplete, - Provision: []*proto.Provision_Response{{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "example", - Type: "aws_instance", - Agent: &proto.Agent{ - Id: uuid.NewString(), - Auth: &proto.Agent_Token{ - Token: authToken, - }, - }, - }}, - }, - }, - }}, - }) - template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) - coderdtest.AwaitTemplateVersionJob(t, client, version.ID) - workspace := coderdtest.CreateWorkspace(t, client, codersdk.Me, template.ID) - coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID) - daemonCloser.Close() - - agentClient := codersdk.New(client.URL) - agentClient.SessionToken = authToken - agentCloser := agent.New(agentClient.ListenWorkspaceAgent, &peer.ConnOptions{ - Logger: slogtest.Make(t, nil), - }) - t.Cleanup(func() { - _ = agentCloser.Close() - }) - resources := coderdtest.AwaitWorkspaceAgents(t, client, workspace.LatestBuild.ID) - conn, err := client.DialWorkspaceAgent(context.Background(), resources[0].ID, nil, &peer.ConnOptions{ - Logger: slogtest.Make(t, nil).Named("client").Leveled(slog.LevelDebug), - }) - require.NoError(t, err) - t.Cleanup(func() { - _ = conn.Close() - }) - _, err = conn.Ping() - require.NoError(t, err) -} diff --git a/codersdk/gitsshkey.go b/codersdk/gitsshkey.go index b7944ab53e..bae8a4c343 100644 --- a/codersdk/gitsshkey.go +++ b/codersdk/gitsshkey.go @@ -56,7 +56,7 @@ func (c *Client) RegenerateGitSSHKey(ctx context.Context, userID uuid.UUID) (Git // AgentGitSSHKey will return the user's SSH key pair for the workspace. func (c *Client) AgentGitSSHKey(ctx context.Context) (AgentGitSSHKey, error) { - res, err := c.request(ctx, http.MethodGet, "/api/v2/workspaceresources/agent/gitsshkey", nil) + res, err := c.request(ctx, http.MethodGet, "/api/v2/workspaceagents/me/gitsshkey", nil) if err != nil { return AgentGitSSHKey{}, xerrors.Errorf("execute request: %w", err) } diff --git a/codersdk/workspaceresourceauth.go b/codersdk/workspaceagents.go similarity index 51% rename from codersdk/workspaceresourceauth.go rename to codersdk/workspaceagents.go index 721a9ca487..5a83d0b265 100644 --- a/codersdk/workspaceresourceauth.go +++ b/codersdk/workspaceagents.go @@ -6,9 +6,21 @@ import ( "fmt" "io" "net/http" + "net/http/cookiejar" "cloud.google.com/go/compute/metadata" + "github.com/google/uuid" + "github.com/hashicorp/yamux" + "github.com/pion/webrtc/v3" "golang.org/x/xerrors" + "nhooyr.io/websocket" + + "github.com/coder/coder/agent" + "github.com/coder/coder/coderd/httpmw" + "github.com/coder/coder/peer" + "github.com/coder/coder/peerbroker" + "github.com/coder/coder/peerbroker/proto" + "github.com/coder/coder/provisionersdk" ) type GoogleInstanceIdentityToken struct { @@ -43,7 +55,7 @@ func (c *Client) AuthWorkspaceGoogleInstanceIdentity(ctx context.Context, servic if err != nil { return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("get metadata identity: %w", err) } - res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/google-instance-identity", GoogleInstanceIdentityToken{ + res, err := c.request(ctx, http.MethodPost, "/api/v2/workspaceagents/google-instance-identity", GoogleInstanceIdentityToken{ JSONWebToken: jwt, }) if err != nil { @@ -107,7 +119,7 @@ func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (Workspac return WorkspaceAgentAuthenticateResponse{}, xerrors.Errorf("read token: %w", err) } - res, err = c.request(ctx, http.MethodPost, "/api/v2/workspaceresources/auth/aws-instance-identity", AWSInstanceIdentityToken{ + res, err = c.request(ctx, http.MethodPost, "/api/v2/workspaceagents/aws-instance-identity", AWSInstanceIdentityToken{ Signature: string(signature), Document: string(document), }) @@ -121,3 +133,108 @@ func (c *Client) AuthWorkspaceAWSInstanceIdentity(ctx context.Context) (Workspac var resp WorkspaceAgentAuthenticateResponse return resp, json.NewDecoder(res.Body).Decode(&resp) } + +// ListenWorkspaceAgent connects as a workspace agent. +// It obtains the agent ID based off the session token. +func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { + serverURL, err := c.URL.Parse("/api/v2/workspaceagents/me") + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(serverURL, []*http.Cookie{{ + Name: httpmw.AuthCookie, + Value: c.SessionToken, + }}) + httpClient := &http.Client{ + Jar: jar, + } + conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + // Need to disable compression to avoid a data-race. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + if res == nil { + return nil, err + } + return nil, readBodyAsError(res) + } + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config) + if err != nil { + return nil, xerrors.Errorf("multiplex client: %w", err) + } + return peerbroker.Listen(session, func(ctx context.Context) ([]webrtc.ICEServer, error) { + return []webrtc.ICEServer{{ + URLs: []string{"stun:stun.l.google.com:19302"}, + }}, nil + }, opts) +} + +// DialWorkspaceAgent creates a connection to the specified resource. +func (c *Client) DialWorkspaceAgent(ctx context.Context, agentID uuid.UUID, iceServers []webrtc.ICEServer, opts *peer.ConnOptions) (*agent.Conn, error) { + serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceagents/%s/dial", agentID.String())) + if err != nil { + return nil, xerrors.Errorf("parse url: %w", err) + } + jar, err := cookiejar.New(nil) + if err != nil { + return nil, xerrors.Errorf("create cookie jar: %w", err) + } + jar.SetCookies(serverURL, []*http.Cookie{{ + Name: httpmw.AuthCookie, + Value: c.SessionToken, + }}) + httpClient := &http.Client{ + Jar: jar, + } + conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ + HTTPClient: httpClient, + // Need to disable compression to avoid a data-race. + CompressionMode: websocket.CompressionDisabled, + }) + if err != nil { + if res == nil { + return nil, err + } + return nil, readBodyAsError(res) + } + config := yamux.DefaultConfig() + config.LogOutput = io.Discard + session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config) + if err != nil { + return nil, xerrors.Errorf("multiplex client: %w", err) + } + client := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)) + stream, err := client.NegotiateConnection(ctx) + if err != nil { + return nil, xerrors.Errorf("negotiate connection: %w", err) + } + peerConn, err := peerbroker.Dial(stream, iceServers, opts) + if err != nil { + return nil, xerrors.Errorf("dial peer: %w", err) + } + return &agent.Conn{ + Negotiator: client, + Conn: peerConn, + }, nil +} + +// WorkspaceAgent returns an agent by ID. +func (c *Client) WorkspaceAgent(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) { + res, err := c.request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/workspaceagents/%s", id), nil) + if err != nil { + return WorkspaceAgent{}, err + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + return WorkspaceAgent{}, readBodyAsError(res) + } + var workspaceAgent WorkspaceAgent + return workspaceAgent, json.NewDecoder(res.Body).Decode(&workspaceAgent) +} diff --git a/codersdk/workspaceresources.go b/codersdk/workspaceresources.go index 593026b754..e5b818ccb4 100644 --- a/codersdk/workspaceresources.go +++ b/codersdk/workspaceresources.go @@ -4,24 +4,12 @@ import ( "context" "encoding/json" "fmt" - "io" "net/http" - "net/http/cookiejar" "time" "github.com/google/uuid" - "github.com/hashicorp/yamux" - "github.com/pion/webrtc/v3" - "golang.org/x/xerrors" - "nhooyr.io/websocket" - "github.com/coder/coder/agent" "github.com/coder/coder/coderd/database" - "github.com/coder/coder/coderd/httpmw" - "github.com/coder/coder/peer" - "github.com/coder/coder/peerbroker" - "github.com/coder/coder/peerbroker/proto" - "github.com/coder/coder/provisionersdk" ) type WorkspaceAgentStatus string @@ -40,7 +28,7 @@ type WorkspaceResource struct { Address string `json:"address"` Type string `json:"type"` Name string `json:"name"` - Agent *WorkspaceAgent `json:"agent,omitempty"` + Agents []WorkspaceAgent `json:"agents,omitempty"` } type WorkspaceAgent struct { @@ -51,9 +39,12 @@ type WorkspaceAgent struct { LastConnectedAt *time.Time `json:"last_connected_at,omitempty"` DisconnectedAt *time.Time `json:"disconnected_at,omitempty"` Status WorkspaceAgentStatus `json:"status"` + Name string `json:"name"` ResourceID uuid.UUID `json:"resource_id"` InstanceID string `json:"instance_id,omitempty"` + Architecture string `json:"architecture"` EnvironmentVariables map[string]string `json:"environment_variables"` + OperatingSystem string `json:"operating_system"` StartupScript string `json:"startup_script,omitempty"` } @@ -89,94 +80,3 @@ func (c *Client) WorkspaceResource(ctx context.Context, id uuid.UUID) (Workspace var resource WorkspaceResource return resource, json.NewDecoder(res.Body).Decode(&resource) } - -// DialWorkspaceAgent creates a connection to the specified resource. -func (c *Client) DialWorkspaceAgent(ctx context.Context, resource uuid.UUID, iceServers []webrtc.ICEServer, opts *peer.ConnOptions) (*agent.Conn, error) { - serverURL, err := c.URL.Parse(fmt.Sprintf("/api/v2/workspaceresources/%s/dial", resource.String())) - if err != nil { - return nil, xerrors.Errorf("parse url: %w", err) - } - jar, err := cookiejar.New(nil) - if err != nil { - return nil, xerrors.Errorf("create cookie jar: %w", err) - } - jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, - Value: c.SessionToken, - }}) - httpClient := &http.Client{ - Jar: jar, - } - conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ - HTTPClient: httpClient, - // Need to disable compression to avoid a data-race. - CompressionMode: websocket.CompressionDisabled, - }) - if err != nil { - if res == nil { - return nil, err - } - return nil, readBodyAsError(res) - } - config := yamux.DefaultConfig() - config.LogOutput = io.Discard - session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config) - if err != nil { - return nil, xerrors.Errorf("multiplex client: %w", err) - } - client := proto.NewDRPCPeerBrokerClient(provisionersdk.Conn(session)) - stream, err := client.NegotiateConnection(ctx) - if err != nil { - return nil, xerrors.Errorf("negotiate connection: %w", err) - } - peerConn, err := peerbroker.Dial(stream, iceServers, opts) - if err != nil { - return nil, xerrors.Errorf("dial peer: %w", err) - } - return &agent.Conn{ - Negotiator: client, - Conn: peerConn, - }, nil -} - -// ListenWorkspaceAgent connects as a workspace agent. -// It obtains the agent ID based off the session token. -func (c *Client) ListenWorkspaceAgent(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) { - serverURL, err := c.URL.Parse("/api/v2/workspaceresources/agent") - if err != nil { - return nil, xerrors.Errorf("parse url: %w", err) - } - jar, err := cookiejar.New(nil) - if err != nil { - return nil, xerrors.Errorf("create cookie jar: %w", err) - } - jar.SetCookies(serverURL, []*http.Cookie{{ - Name: httpmw.AuthCookie, - Value: c.SessionToken, - }}) - httpClient := &http.Client{ - Jar: jar, - } - conn, res, err := websocket.Dial(ctx, serverURL.String(), &websocket.DialOptions{ - HTTPClient: httpClient, - // Need to disable compression to avoid a data-race. - CompressionMode: websocket.CompressionDisabled, - }) - if err != nil { - if res == nil { - return nil, err - } - return nil, readBodyAsError(res) - } - config := yamux.DefaultConfig() - config.LogOutput = io.Discard - session, err := yamux.Client(websocket.NetConn(ctx, conn, websocket.MessageBinary), config) - if err != nil { - return nil, xerrors.Errorf("multiplex client: %w", err) - } - return peerbroker.Listen(session, func(ctx context.Context) ([]webrtc.ICEServer, error) { - return []webrtc.ICEServer{{ - URLs: []string{"stun:stun.l.google.com:19302"}, - }}, nil - }, opts) -} diff --git a/examples/aws-linux/main.tf b/examples/aws-linux/main.tf index 6dfa861656..7a35095dbb 100644 --- a/examples/aws-linux/main.tf +++ b/examples/aws-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.2.1" + version = "~> 0.3.1" } } } @@ -55,12 +55,6 @@ provider "aws" { data "coder_workspace" "me" { } -data "coder_agent_script" "dev" { - arch = "amd64" - auth = "aws-instance-identity" - os = "linux" -} - data "aws_ami" "ubuntu" { most_recent = true filter { @@ -75,8 +69,9 @@ data "aws_ami" "ubuntu" { } resource "coder_agent" "dev" { - count = data.coder_workspace.me.transition == "start" ? 1 : 0 - instance_id = aws_instance.dev[0].id + arch = "amd64" + auth = "aws-instance-identity" + os = "linux" } locals { @@ -105,7 +100,7 @@ Content-Transfer-Encoding: 7bit Content-Disposition: attachment; filename="userdata.txt" #!/bin/bash -sudo -E -u ubuntu sh -c '${data.coder_agent_script.dev.value}' +sudo -E -u ubuntu sh -c '${coder_agent.dev.init_script}' --//-- EOT @@ -139,11 +134,9 @@ resource "aws_instance" "dev" { ami = data.aws_ami.ubuntu.id availability_zone = "${var.region}a" instance_type = "t3.micro" - count = 1 user_data = data.coder_workspace.me.transition == "start" ? local.user_data_start : local.user_data_end tags = { Name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}" } - } diff --git a/examples/aws-windows/main.tf b/examples/aws-windows/main.tf index a7104e5425..575ee75b8e 100644 --- a/examples/aws-windows/main.tf +++ b/examples/aws-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.2.1" + version = "~> 0.3.1" } } } @@ -42,12 +42,6 @@ provider "aws" { data "coder_workspace" "me" { } -data "coder_agent_script" "dev" { - arch = "amd64" - auth = "aws-instance-identity" - os = "windows" -} - data "aws_ami" "windows" { most_recent = true owners = ["amazon"] @@ -59,8 +53,9 @@ data "aws_ami" "windows" { } resource "coder_agent" "dev" { - count = data.coder_workspace.me.transition == "start" ? 1 : 0 - instance_id = aws_instance.dev[0].id + arch = "amd64" + auth = "aws-instance-identity" + os = "windows" } locals { @@ -71,7 +66,7 @@ locals { user_data_start = < [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -${data.coder_agent_script.dev.value} +${coder_agent.dev.init_script} true EOT diff --git a/examples/gcp-linux/main.tf b/examples/gcp-linux/main.tf index b3cf30a5cd..ef069a380a 100644 --- a/examples/gcp-linux/main.tf +++ b/examples/gcp-linux/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 0.2" + version = "~> 0.3.1" } google = { source = "hashicorp/google" @@ -42,25 +42,14 @@ provider "google" { project = jsondecode(var.service_account).project_id } -data "coder_workspace" "me" { -} - -data "coder_agent_script" "dev" { - auth = "google-instance-identity" - arch = "amd64" - os = "linux" -} - data "google_compute_default_service_account" "default" { } -resource "random_string" "random" { - length = 8 - special = false +data "coder_workspace" "me" { } resource "google_compute_disk" "root" { - name = "coder-${lower(random_string.random.result)}" + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" type = "pd-ssd" zone = var.zone image = "debian-cloud/debian-9" @@ -69,10 +58,16 @@ resource "google_compute_disk" "root" { } } +resource "coder_agent" "dev" { + auth = "google-instance-identity" + arch = "amd64" + os = "linux" +} + resource "google_compute_instance" "dev" { zone = var.zone - count = data.coder_workspace.me.transition == "start" ? 1 : 0 - name = "coder-${lower(random_string.random.result)}" + count = data.coder_workspace.me.start_count + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}" machine_type = "e2-medium" network_interface { network = "default" @@ -88,10 +83,5 @@ resource "google_compute_instance" "dev" { email = data.google_compute_default_service_account.default.email scopes = ["cloud-platform"] } - metadata_startup_script = data.coder_agent_script.dev.value -} - -resource "coder_agent" "dev" { - count = length(google_compute_instance.dev) - instance_id = google_compute_instance.dev[0].instance_id + metadata_startup_script = coder_agent.dev.init_script } diff --git a/examples/gcp-windows/main.tf b/examples/gcp-windows/main.tf index fe466d7561..2202ba8889 100644 --- a/examples/gcp-windows/main.tf +++ b/examples/gcp-windows/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 0.2" + version = "~> 0.3.1" } google = { source = "hashicorp/google" @@ -45,22 +45,11 @@ provider "google" { data "coder_workspace" "me" { } -data "coder_agent_script" "dev" { - auth = "google-instance-identity" - arch = "amd64" - os = "windows" -} - data "google_compute_default_service_account" "default" { } -resource "random_string" "random" { - length = 8 - special = false -} - resource "google_compute_disk" "root" { - name = "coder-${lower(random_string.random.result)}" + name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}-root" type = "pd-ssd" zone = var.zone image = "projects/windows-cloud/global/images/windows-server-2022-dc-core-v20220215" @@ -69,9 +58,15 @@ resource "google_compute_disk" "root" { } } +resource "coder_agent" "dev" { + auth = "google-instance-identity" + arch = "amd64" + os = "windows" +} + resource "google_compute_instance" "dev" { zone = var.zone - count = data.coder_workspace.me.transition == "start" ? 1 : 0 + count = data.coder_workspace.me.start_count name = "coder-${data.coder_workspace.me.owner}-${data.coder_workspace.me.name}" machine_type = "e2-medium" network_interface { @@ -89,12 +84,7 @@ resource "google_compute_instance" "dev" { scopes = ["cloud-platform"] } metadata = { - windows-startup-script-ps1 = data.coder_agent_script.dev.value + windows-startup-script-ps1 = coder_agent.dev.init_script serial-port-enable = "TRUE" } } - -resource "coder_agent" "dev" { - count = length(google_compute_instance.dev) - instance_id = google_compute_instance.dev[0].instance_id -} diff --git a/provisioner/terraform/provision.go b/provisioner/terraform/provision.go index a962269d64..b9f6fdaa7e 100644 --- a/provisioner/terraform/provision.go +++ b/provisioner/terraform/provision.go @@ -286,12 +286,32 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi continue } agent := &proto.Agent{ + Name: resource.Name, Auth: &proto.Agent_Token{}, } - if envRaw, has := resource.Expressions["env"]; has { - env, ok := envRaw.ConstantValue.(map[string]string) + if operatingSystemRaw, has := resource.Expressions["os"]; has { + operatingSystem, ok := operatingSystemRaw.ConstantValue.(string) if ok { - agent.Env = env + agent.OperatingSystem = operatingSystem + } + } + if archRaw, has := resource.Expressions["arch"]; has { + arch, ok := archRaw.ConstantValue.(string) + if ok { + agent.Architecture = arch + } + } + if envRaw, has := resource.Expressions["env"]; has { + env, ok := envRaw.ConstantValue.(map[string]interface{}) + if ok { + agent.Env = map[string]string{} + for key, valueRaw := range env { + value, valid := valueRaw.(string) + if !valid { + continue + } + agent.Env[key] = value + } } } if startupScriptRaw, has := resource.Expressions["startup_script"]; has { @@ -320,19 +340,20 @@ func parseTerraformPlan(ctx context.Context, terraform *tfexec.Terraform, planfi continue } // Associate resources that depend on an agent. - var agent *proto.Agent + resourceAgents := make([]*proto.Agent, 0) for _, dep := range resourceNode { var has bool - agent, has = agents[dep] - if has { - break + agent, has := agents[dep] + if !has { + continue } + resourceAgents = append(resourceAgents, agent) } resources = append(resources, &proto.Resource{ - Name: resource.Name, - Type: resource.Type, - Agent: agent, + Name: resource.Name, + Type: resource.Type, + Agents: resourceAgents, }) } @@ -365,11 +386,13 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state return nil, xerrors.Errorf("find dependencies: %w", err) } type agentAttributes struct { - ID string `mapstructure:"id"` - Token string `mapstructure:"token"` - InstanceID string `mapstructure:"instance_id"` - Env map[string]string `mapstructure:"env"` - StartupScript string `mapstructure:"startup_script"` + Auth string `mapstructure:"auth"` + OperatingSystem string `mapstructure:"os"` + Architecture string `mapstructure:"arch"` + ID string `mapstructure:"id"` + Token string `mapstructure:"token"` + Env map[string]string `mapstructure:"env"` + StartupScript string `mapstructure:"startup_script"` } agents := map[string]*proto.Agent{} @@ -384,24 +407,60 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state return nil, xerrors.Errorf("decode agent attributes: %w", err) } agent := &proto.Agent{ - Id: attrs.ID, - Env: attrs.Env, - StartupScript: attrs.StartupScript, - Auth: &proto.Agent_Token{ - Token: attrs.Token, - }, + Name: resource.Name, + Id: attrs.ID, + Env: attrs.Env, + StartupScript: attrs.StartupScript, + OperatingSystem: attrs.OperatingSystem, + Architecture: attrs.Architecture, } - if attrs.InstanceID != "" { - agent.Auth = &proto.Agent_InstanceId{ - InstanceId: attrs.InstanceID, + switch attrs.Auth { + case "token": + agent.Auth = &proto.Agent_Token{ + Token: attrs.Token, } + default: + agent.Auth = &proto.Agent_InstanceId{} } resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") agents[resourceKey] = agent } + // Manually associate agents with instance IDs. for _, resource := range state.Values.RootModule.Resources { - if resource.Type == "coder_agent" { + if resource.Type != "coder_agent_instance" { + continue + } + agentIDRaw, valid := resource.AttributeValues["agent_id"] + if !valid { + continue + } + agentID, valid := agentIDRaw.(string) + if !valid { + continue + } + instanceIDRaw, valid := resource.AttributeValues["instance_id"] + if !valid { + continue + } + instanceID, valid := instanceIDRaw.(string) + if !valid { + continue + } + + for _, agent := range agents { + if agent.Id != agentID { + continue + } + agent.Auth = &proto.Agent_InstanceId{ + InstanceId: instanceID, + } + break + } + } + + for _, resource := range state.Values.RootModule.Resources { + if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" { continue } resourceKey := strings.Join([]string{resource.Type, resource.Name}, ".") @@ -410,19 +469,46 @@ func parseTerraformApply(ctx context.Context, terraform *tfexec.Terraform, state continue } // Associate resources that depend on an agent. - var agent *proto.Agent + resourceAgents := make([]*proto.Agent, 0) for _, dep := range resourceNode { var has bool - agent, has = agents[dep] - if has { - break + agent, has := agents[dep] + if !has { + continue + } + resourceAgents = append(resourceAgents, agent) + + // Didn't use instance identity. + if agent.GetToken() != "" { + continue + } + + key, isValid := map[string]string{ + "google_compute_instance": "instance_id", + "aws_instance": "id", + }[resource.Type] + if !isValid { + // The resource type doesn't support + // automatically setting the instance ID. + continue + } + instanceIDRaw, valid := resource.AttributeValues[key] + if !valid { + continue + } + instanceID, valid := instanceIDRaw.(string) + if !valid { + continue + } + agent.Auth = &proto.Agent_InstanceId{ + InstanceId: instanceID, } } resources = append(resources, &proto.Resource{ - Name: resource.Name, - Type: resource.Type, - Agent: agent, + Name: resource.Name, + Type: resource.Type, + Agents: resourceAgents, }) } } @@ -467,9 +553,8 @@ func convertTerraformLogLevel(logLevel string) (proto.LogLevel, error) { } } -// findDirectDependencies maps Terraform resources to their parent and -// children nodes. This parses GraphViz output from Terraform which -// certainly is not ideal, but seems reliable. +// findDirectDependencies maps Terraform resources to their children nodes. +// This parses GraphViz output from Terraform which isn't ideal, but seems reliable. func findDirectDependencies(rawGraph string) (map[string][]string, error) { parsedGraph, err := gographviz.ParseString(rawGraph) if err != nil { @@ -488,22 +573,17 @@ func findDirectDependencies(rawGraph string) (map[string][]string, error) { label = strings.Trim(label, `"`) dependencies := make([]string, 0) - for _, edges := range []map[string][]*gographviz.Edge{ - graph.Edges.SrcToDsts[node.Name], - graph.Edges.DstToSrcs[node.Name], - } { - for destination := range edges { - dependencyNode, exists := graph.Nodes.Lookup[destination] - if !exists { - continue - } - label, exists := dependencyNode.Attrs["label"] - if !exists { - continue - } - label = strings.Trim(label, `"`) - dependencies = append(dependencies, label) + for destination := range graph.Edges.SrcToDsts[node.Name] { + dependencyNode, exists := graph.Nodes.Lookup[destination] + if !exists { + continue } + label, exists := dependencyNode.Attrs["label"] + if !exists { + continue + } + label = strings.Trim(label, `"`) + dependencies = append(dependencies, label) } direct[label] = dependencies } diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 3ecd1e72ac..645c87ba03 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -7,6 +7,7 @@ import ( "encoding/json" "os" "path/filepath" + "sort" "testing" "github.com/stretchr/testify/require" @@ -27,7 +28,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "0.2.1" + version = "0.3.1" } } } @@ -156,7 +157,10 @@ provider "coder" { Name: "resource-associated-with-agent", Files: map[string]string{ "main.tf": provider + ` - resource "coder_agent" "A" {} + resource "coder_agent" "A" { + os = "windows" + arch = "arm64" + } resource "null_resource" "A" { depends_on = [ coder_agent.A @@ -176,45 +180,14 @@ provider "coder" { Resources: []*proto.Resource{{ Name: "A", Type: "null_resource", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "windows", + Architecture: "arm64", Auth: &proto.Agent_Token{ Token: "", }, - }, - }}, - }, - }, - }, - }, { - Name: "agent-associated-with-resource", - Files: map[string]string{ - "main.tf": provider + ` - resource "coder_agent" "A" { - depends_on = [ - null_resource.A - ] - instance_id = "example" - } - resource "null_resource" "A" {}`, - }, - Request: &proto.Provision_Request{ - Type: &proto.Provision_Request_Start{ - Start: &proto.Provision_Start{ - Metadata: &proto.Provision_Metadata{}, - }, - }, - }, - Response: &proto.Provision_Response{ - Type: &proto.Provision_Response_Complete{ - Complete: &proto.Provision_Complete{ - Resources: []*proto.Resource{{ - Name: "A", - Type: "null_resource", - Agent: &proto.Agent{ - Auth: &proto.Agent_InstanceId{ - InstanceId: "example", - }, - }, + }}, }}, }, }, @@ -225,6 +198,12 @@ provider "coder" { "main.tf": provider + ` resource "coder_agent" "A" { count = 1 + os = "linux" + arch = "amd64" + env = { + test: "example" + } + startup_script = "code-server" } resource "null_resource" "A" { count = length(coder_agent.A) @@ -244,29 +223,42 @@ provider "coder" { Resources: []*proto.Resource{{ Name: "A", Type: "null_resource", - Agent: &proto.Agent{ - Auth: &proto.Agent_Token{}, - }, + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{}, + Env: map[string]string{ + "test": "example", + }, + StartupScript: "code-server", + }}, }}, }, }, }, }, { - Name: "dryrun-agent-associated-with-resource", + Name: "resource-manually-associated-with-agent", Files: map[string]string{ "main.tf": provider + ` resource "coder_agent" "A" { - count = length(null_resource.A) - instance_id = "an-instance" + os = "darwin" + arch = "amd64" } resource "null_resource" "A" { - count = 1 - }`, + depends_on = [ + coder_agent.A + ] + } + resource "coder_agent_instance" "A" { + agent_id = coder_agent.A.id + instance_id = "bananas" + } + `, }, Request: &proto.Provision_Request{ Type: &proto.Provision_Request_Start{ Start: &proto.Provision_Start{ - DryRun: true, Metadata: &proto.Provision_Metadata{}, }, }, @@ -277,31 +269,45 @@ provider "coder" { Resources: []*proto.Resource{{ Name: "A", Type: "null_resource", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "darwin", + Architecture: "amd64", Auth: &proto.Agent_InstanceId{ - InstanceId: "", + InstanceId: "bananas", }, - }, + }}, }}, }, }, }, }, { - Name: "dryrun-agent-associated-with-resource-instance-id", + Name: "resource-manually-associated-with-multiple-agents", Files: map[string]string{ "main.tf": provider + ` resource "coder_agent" "A" { - count = length(null_resource.A) - instance_id = length(null_resource.A) + os = "darwin" + arch = "amd64" + } + resource "coder_agent" "B" { + os = "linux" + arch = "amd64" } resource "null_resource" "A" { - count = 1 - }`, + depends_on = [ + coder_agent.A, + coder_agent.B + ] + } + resource "coder_agent_instance" "A" { + agent_id = coder_agent.A.id + instance_id = "bananas" + } + `, }, Request: &proto.Provision_Request{ Type: &proto.Provision_Request_Start{ Start: &proto.Provision_Start{ - DryRun: true, Metadata: &proto.Provision_Metadata{}, }, }, @@ -312,11 +318,21 @@ provider "coder" { Resources: []*proto.Resource{{ Name: "A", Type: "null_resource", - Agent: &proto.Agent{ + Agents: []*proto.Agent{{ + Name: "A", + OperatingSystem: "darwin", + Architecture: "amd64", Auth: &proto.Agent_InstanceId{ - InstanceId: "", + InstanceId: "bananas", }, - }, + }, { + Name: "B", + OperatingSystem: "linux", + Architecture: "amd64", + Auth: &proto.Agent_Token{ + Token: "", + }, + }}, }}, }, }, @@ -375,15 +391,16 @@ provider "coder" { // Remove randomly generated data. for _, resource := range msg.GetComplete().Resources { - if resource.Agent == nil { - continue - } - resource.Agent.Id = "" - if resource.Agent.GetToken() == "" { - continue - } - resource.Agent.Auth = &proto.Agent_Token{ - Token: "", + sort.Slice(resource.Agents, func(i, j int) bool { + return resource.Agents[i].Name < resource.Agents[j].Name + }) + + for _, agent := range resource.Agents { + agent.Id = "" + if agent.GetToken() == "" { + continue + } + agent.Auth = &proto.Agent_Token{} } } diff --git a/provisionersdk/proto/provisioner.pb.go b/provisionersdk/proto/provisioner.pb.go index c5617f74cd..72d37a0083 100644 --- a/provisionersdk/proto/provisioner.pb.go +++ b/provisionersdk/proto/provisioner.pb.go @@ -651,7 +651,7 @@ func (x *Log) GetOutput() string { return "" } -type GoogleInstanceIdentityAuth struct { +type InstanceIdentityAuth struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields @@ -659,8 +659,8 @@ type GoogleInstanceIdentityAuth struct { InstanceId string `protobuf:"bytes,1,opt,name=instance_id,json=instanceId,proto3" json:"instance_id,omitempty"` } -func (x *GoogleInstanceIdentityAuth) Reset() { - *x = GoogleInstanceIdentityAuth{} +func (x *InstanceIdentityAuth) Reset() { + *x = InstanceIdentityAuth{} if protoimpl.UnsafeEnabled { mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -668,13 +668,13 @@ func (x *GoogleInstanceIdentityAuth) Reset() { } } -func (x *GoogleInstanceIdentityAuth) String() string { +func (x *InstanceIdentityAuth) String() string { return protoimpl.X.MessageStringOf(x) } -func (*GoogleInstanceIdentityAuth) ProtoMessage() {} +func (*InstanceIdentityAuth) ProtoMessage() {} -func (x *GoogleInstanceIdentityAuth) ProtoReflect() protoreflect.Message { +func (x *InstanceIdentityAuth) ProtoReflect() protoreflect.Message { mi := &file_provisionersdk_proto_provisioner_proto_msgTypes[6] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -686,12 +686,12 @@ func (x *GoogleInstanceIdentityAuth) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use GoogleInstanceIdentityAuth.ProtoReflect.Descriptor instead. -func (*GoogleInstanceIdentityAuth) Descriptor() ([]byte, []int) { +// Deprecated: Use InstanceIdentityAuth.ProtoReflect.Descriptor instead. +func (*InstanceIdentityAuth) Descriptor() ([]byte, []int) { return file_provisionersdk_proto_provisioner_proto_rawDescGZIP(), []int{6} } -func (x *GoogleInstanceIdentityAuth) GetInstanceId() string { +func (x *InstanceIdentityAuth) GetInstanceId() string { if x != nil { return x.InstanceId } @@ -704,9 +704,12 @@ type Agent struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` - Env map[string]string `protobuf:"bytes,2,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` - StartupScript string `protobuf:"bytes,3,opt,name=startup_script,json=startupScript,proto3" json:"startup_script,omitempty"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Env map[string]string `protobuf:"bytes,3,rep,name=env,proto3" json:"env,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` + StartupScript string `protobuf:"bytes,4,opt,name=startup_script,json=startupScript,proto3" json:"startup_script,omitempty"` + OperatingSystem string `protobuf:"bytes,5,opt,name=operating_system,json=operatingSystem,proto3" json:"operating_system,omitempty"` + Architecture string `protobuf:"bytes,6,opt,name=architecture,proto3" json:"architecture,omitempty"` // Types that are assignable to Auth: // *Agent_Token // *Agent_InstanceId @@ -752,6 +755,13 @@ func (x *Agent) GetId() string { return "" } +func (x *Agent) GetName() string { + if x != nil { + return x.Name + } + return "" +} + func (x *Agent) GetEnv() map[string]string { if x != nil { return x.Env @@ -766,6 +776,20 @@ func (x *Agent) GetStartupScript() string { return "" } +func (x *Agent) GetOperatingSystem() string { + if x != nil { + return x.OperatingSystem + } + return "" +} + +func (x *Agent) GetArchitecture() string { + if x != nil { + return x.Architecture + } + return "" +} + func (m *Agent) GetAuth() isAgent_Auth { if m != nil { return m.Auth @@ -792,11 +816,11 @@ type isAgent_Auth interface { } type Agent_Token struct { - Token string `protobuf:"bytes,4,opt,name=token,proto3,oneof"` + Token string `protobuf:"bytes,7,opt,name=token,proto3,oneof"` } type Agent_InstanceId struct { - InstanceId string `protobuf:"bytes,5,opt,name=instance_id,json=instanceId,proto3,oneof"` + InstanceId string `protobuf:"bytes,8,opt,name=instance_id,json=instanceId,proto3,oneof"` } func (*Agent_Token) isAgent_Auth() {} @@ -809,9 +833,9 @@ type Resource struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` - Agent *Agent `protobuf:"bytes,3,opt,name=agent,proto3" json:"agent,omitempty"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"` + Agents []*Agent `protobuf:"bytes,3,rep,name=agents,proto3" json:"agents,omitempty"` } func (x *Resource) Reset() { @@ -860,9 +884,9 @@ func (x *Resource) GetType() string { return "" } -func (x *Resource) GetAgent() *Agent { +func (x *Resource) GetAgents() []*Agent { if x != nil { - return x.Agent + return x.Agents } return nil } @@ -1609,119 +1633,125 @@ var file_provisionersdk_proto_provisioner_proto_rawDesc = []byte{ 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, 0x6c, 0x52, 0x05, 0x6c, 0x65, 0x76, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, - 0x70, 0x75, 0x74, 0x22, 0x3d, 0x0a, 0x1a, 0x47, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x49, 0x6e, 0x73, - 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, - 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, - 0x49, 0x64, 0x22, 0xe8, 0x01, 0x0a, 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, - 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x2d, 0x0a, 0x03, - 0x65, 0x6e, 0x76, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, - 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x25, 0x0a, 0x0e, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x53, 0x63, 0x72, 0x69, - 0x70, 0x74, 0x12, 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, - 0x09, 0x48, 0x00, 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, - 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x48, - 0x00, 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 0x0a, - 0x08, 0x45, 0x6e, 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, - 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, - 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x5c, 0x0a, - 0x08, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, - 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, - 0x65, 0x12, 0x28, 0x0a, 0x05, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x12, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, - 0x67, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x22, 0xfc, 0x01, 0x0a, 0x05, - 0x50, 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x70, 0x75, 0x74, 0x22, 0x37, 0x0a, 0x14, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, + 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x69, + 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0a, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x22, 0xcb, 0x02, 0x0a, + 0x05, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x2d, 0x0a, 0x03, 0x65, 0x6e, + 0x76, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, 0x6e, 0x74, 0x2e, 0x45, 0x6e, 0x76, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x52, 0x03, 0x65, 0x6e, 0x76, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x74, 0x61, + 0x72, 0x74, 0x75, 0x70, 0x5f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0d, 0x73, 0x74, 0x61, 0x72, 0x74, 0x75, 0x70, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, + 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x70, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x79, + 0x73, 0x74, 0x65, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x70, 0x65, 0x72, + 0x61, 0x74, 0x69, 0x6e, 0x67, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x12, 0x22, 0x0a, 0x0c, 0x61, + 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x0c, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x12, + 0x16, 0x0a, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, + 0x52, 0x05, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x21, 0x0a, 0x0b, 0x69, 0x6e, 0x73, 0x74, 0x61, + 0x6e, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x49, 0x64, 0x1a, 0x36, 0x0a, 0x08, 0x45, 0x6e, + 0x76, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x42, 0x06, 0x0a, 0x04, 0x61, 0x75, 0x74, 0x68, 0x22, 0x5e, 0x0a, 0x08, 0x52, 0x65, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x2a, + 0x0a, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x41, 0x67, 0x65, + 0x6e, 0x74, 0x52, 0x06, 0x61, 0x67, 0x65, 0x6e, 0x74, 0x73, 0x22, 0xfc, 0x01, 0x0a, 0x05, 0x50, + 0x61, 0x72, 0x73, 0x65, 0x1a, 0x27, 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, + 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, 0x0a, + 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, 0x72, + 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x02, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, 0x65, + 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, + 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, + 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, + 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, 0x6d, + 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa9, 0x06, 0x0a, 0x09, 0x50, 0x72, + 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xcc, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, 0x61, + 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, 0x72, + 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, 0x72, + 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x74, + 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, 0x6f, + 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, + 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, 0x0a, + 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, 0x72, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x1a, 0x55, - 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x49, 0x0a, 0x11, 0x70, 0x61, - 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, - 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, 0x68, - 0x65, 0x6d, 0x61, 0x52, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x53, 0x63, - 0x68, 0x65, 0x6d, 0x61, 0x73, 0x1a, 0x73, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, - 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, - 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x39, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, - 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x43, 0x6f, - 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xa9, 0x06, 0x0a, 0x09, 0x50, - 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0xcc, 0x01, 0x0a, 0x08, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x5f, 0x75, - 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x55, - 0x72, 0x6c, 0x12, 0x53, 0x0a, 0x14, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, - 0x74, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, - 0x32, 0x20, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x57, - 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x13, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0d, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x27, - 0x0a, 0x0f, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x6f, 0x77, 0x6e, 0x65, - 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x4f, 0x77, 0x6e, 0x65, 0x72, 0x1a, 0xd9, 0x01, 0x0a, 0x05, 0x53, 0x74, 0x61, 0x72, - 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, - 0x46, 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, - 0x72, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, - 0x61, 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, - 0x64, 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, - 0x79, 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, - 0x52, 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, - 0x0a, 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, - 0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x12, 0x46, + 0x0a, 0x10, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, 0x5f, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x0f, 0x70, 0x61, 0x72, 0x61, 0x6d, 0x65, 0x74, 0x65, 0x72, + 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x12, 0x3b, 0x0a, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, - 0x2e, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, - 0x37, 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1d, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, - 0x52, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, - 0x1a, 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, - 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, - 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, - 0x75, 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, - 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, - 0x63, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, - 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, - 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, - 0x3d, 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, - 0x74, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, - 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, - 0x65, 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, - 0x05, 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, - 0x10, 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, - 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, - 0x70, 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, - 0x0a, 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, - 0x50, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, - 0x32, 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, - 0x12, 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, - 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, - 0x6e, 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, - 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, - 0x2f, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x64, 0x72, 0x79, + 0x5f, 0x72, 0x75, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x64, 0x72, 0x79, 0x52, + 0x75, 0x6e, 0x1a, 0x08, 0x0a, 0x06, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x1a, 0x80, 0x01, 0x0a, + 0x07, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x34, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x72, + 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, + 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, + 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x00, 0x52, 0x05, 0x73, 0x74, 0x61, 0x72, 0x74, 0x12, 0x37, + 0x0a, 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1d, + 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x48, 0x00, 0x52, + 0x06, 0x63, 0x61, 0x6e, 0x63, 0x65, 0x6c, 0x42, 0x06, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x1a, + 0x6b, 0x0a, 0x08, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, + 0x74, 0x61, 0x74, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x33, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, + 0x72, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x70, 0x72, 0x6f, + 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, + 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x73, 0x1a, 0x77, 0x0a, 0x08, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x24, 0x0a, 0x03, 0x6c, 0x6f, 0x67, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, + 0x6e, 0x65, 0x72, 0x2e, 0x4c, 0x6f, 0x67, 0x48, 0x00, 0x52, 0x03, 0x6c, 0x6f, 0x67, 0x12, 0x3d, + 0x0a, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x43, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, + 0x65, 0x48, 0x00, 0x52, 0x08, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x42, 0x06, 0x0a, + 0x04, 0x74, 0x79, 0x70, 0x65, 0x2a, 0x3f, 0x0a, 0x08, 0x4c, 0x6f, 0x67, 0x4c, 0x65, 0x76, 0x65, + 0x6c, 0x12, 0x09, 0x0a, 0x05, 0x54, 0x52, 0x41, 0x43, 0x45, 0x10, 0x00, 0x12, 0x09, 0x0a, 0x05, + 0x44, 0x45, 0x42, 0x55, 0x47, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x49, 0x4e, 0x46, 0x4f, 0x10, + 0x02, 0x12, 0x08, 0x0a, 0x04, 0x57, 0x41, 0x52, 0x4e, 0x10, 0x03, 0x12, 0x09, 0x0a, 0x05, 0x45, + 0x52, 0x52, 0x4f, 0x52, 0x10, 0x04, 0x2a, 0x37, 0x0a, 0x13, 0x57, 0x6f, 0x72, 0x6b, 0x73, 0x70, + 0x61, 0x63, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x09, 0x0a, + 0x05, 0x53, 0x54, 0x41, 0x52, 0x54, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x54, 0x4f, 0x50, + 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x44, 0x45, 0x53, 0x54, 0x52, 0x4f, 0x59, 0x10, 0x02, 0x32, + 0xa3, 0x01, 0x0a, 0x0b, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x12, + 0x42, 0x0a, 0x05, 0x50, 0x61, 0x72, 0x73, 0x65, 0x12, 0x1a, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, + 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x65, 0x72, 0x2e, 0x50, 0x61, 0x72, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x30, 0x01, 0x12, 0x50, 0x0a, 0x09, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1e, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x1f, 0x2e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x2e, 0x50, + 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x28, 0x01, 0x30, 0x01, 0x42, 0x2d, 0x5a, 0x2b, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, 0x63, 0x6f, 0x64, 0x65, 0x72, 0x2f, + 0x70, 0x72, 0x6f, 0x76, 0x69, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x72, 0x73, 0x64, 0x6b, 0x2f, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1739,32 +1769,32 @@ func file_provisionersdk_proto_provisioner_proto_rawDescGZIP() []byte { var file_provisionersdk_proto_provisioner_proto_enumTypes = make([]protoimpl.EnumInfo, 5) var file_provisionersdk_proto_provisioner_proto_msgTypes = make([]protoimpl.MessageInfo, 21) var file_provisionersdk_proto_provisioner_proto_goTypes = []interface{}{ - (LogLevel)(0), // 0: provisioner.LogLevel - (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition - (ParameterSource_Scheme)(0), // 2: provisioner.ParameterSource.Scheme - (ParameterDestination_Scheme)(0), // 3: provisioner.ParameterDestination.Scheme - (ParameterSchema_TypeSystem)(0), // 4: provisioner.ParameterSchema.TypeSystem - (*Empty)(nil), // 5: provisioner.Empty - (*ParameterSource)(nil), // 6: provisioner.ParameterSource - (*ParameterDestination)(nil), // 7: provisioner.ParameterDestination - (*ParameterValue)(nil), // 8: provisioner.ParameterValue - (*ParameterSchema)(nil), // 9: provisioner.ParameterSchema - (*Log)(nil), // 10: provisioner.Log - (*GoogleInstanceIdentityAuth)(nil), // 11: provisioner.GoogleInstanceIdentityAuth - (*Agent)(nil), // 12: provisioner.Agent - (*Resource)(nil), // 13: provisioner.Resource - (*Parse)(nil), // 14: provisioner.Parse - (*Provision)(nil), // 15: provisioner.Provision - nil, // 16: provisioner.Agent.EnvEntry - (*Parse_Request)(nil), // 17: provisioner.Parse.Request - (*Parse_Complete)(nil), // 18: provisioner.Parse.Complete - (*Parse_Response)(nil), // 19: provisioner.Parse.Response - (*Provision_Metadata)(nil), // 20: provisioner.Provision.Metadata - (*Provision_Start)(nil), // 21: provisioner.Provision.Start - (*Provision_Cancel)(nil), // 22: provisioner.Provision.Cancel - (*Provision_Request)(nil), // 23: provisioner.Provision.Request - (*Provision_Complete)(nil), // 24: provisioner.Provision.Complete - (*Provision_Response)(nil), // 25: provisioner.Provision.Response + (LogLevel)(0), // 0: provisioner.LogLevel + (WorkspaceTransition)(0), // 1: provisioner.WorkspaceTransition + (ParameterSource_Scheme)(0), // 2: provisioner.ParameterSource.Scheme + (ParameterDestination_Scheme)(0), // 3: provisioner.ParameterDestination.Scheme + (ParameterSchema_TypeSystem)(0), // 4: provisioner.ParameterSchema.TypeSystem + (*Empty)(nil), // 5: provisioner.Empty + (*ParameterSource)(nil), // 6: provisioner.ParameterSource + (*ParameterDestination)(nil), // 7: provisioner.ParameterDestination + (*ParameterValue)(nil), // 8: provisioner.ParameterValue + (*ParameterSchema)(nil), // 9: provisioner.ParameterSchema + (*Log)(nil), // 10: provisioner.Log + (*InstanceIdentityAuth)(nil), // 11: provisioner.InstanceIdentityAuth + (*Agent)(nil), // 12: provisioner.Agent + (*Resource)(nil), // 13: provisioner.Resource + (*Parse)(nil), // 14: provisioner.Parse + (*Provision)(nil), // 15: provisioner.Provision + nil, // 16: provisioner.Agent.EnvEntry + (*Parse_Request)(nil), // 17: provisioner.Parse.Request + (*Parse_Complete)(nil), // 18: provisioner.Parse.Complete + (*Parse_Response)(nil), // 19: provisioner.Parse.Response + (*Provision_Metadata)(nil), // 20: provisioner.Provision.Metadata + (*Provision_Start)(nil), // 21: provisioner.Provision.Start + (*Provision_Cancel)(nil), // 22: provisioner.Provision.Cancel + (*Provision_Request)(nil), // 23: provisioner.Provision.Request + (*Provision_Complete)(nil), // 24: provisioner.Provision.Complete + (*Provision_Response)(nil), // 25: provisioner.Provision.Response } var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 2, // 0: provisioner.ParameterSource.scheme:type_name -> provisioner.ParameterSource.Scheme @@ -1775,7 +1805,7 @@ var file_provisionersdk_proto_provisioner_proto_depIdxs = []int32{ 4, // 5: provisioner.ParameterSchema.validation_type_system:type_name -> provisioner.ParameterSchema.TypeSystem 0, // 6: provisioner.Log.level:type_name -> provisioner.LogLevel 16, // 7: provisioner.Agent.env:type_name -> provisioner.Agent.EnvEntry - 12, // 8: provisioner.Resource.agent:type_name -> provisioner.Agent + 12, // 8: provisioner.Resource.agents:type_name -> provisioner.Agent 9, // 9: provisioner.Parse.Complete.parameter_schemas:type_name -> provisioner.ParameterSchema 10, // 10: provisioner.Parse.Response.log:type_name -> provisioner.Log 18, // 11: provisioner.Parse.Response.complete:type_name -> provisioner.Parse.Complete @@ -1877,7 +1907,7 @@ func file_provisionersdk_proto_provisioner_proto_init() { } } file_provisionersdk_proto_provisioner_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GoogleInstanceIdentityAuth); i { + switch v := v.(*InstanceIdentityAuth); i { case 0: return &v.state case 1: diff --git a/provisionersdk/proto/provisioner.proto b/provisionersdk/proto/provisioner.proto index 2f9e4b1458..84505a6e9e 100644 --- a/provisionersdk/proto/provisioner.proto +++ b/provisionersdk/proto/provisioner.proto @@ -67,18 +67,21 @@ message Log { string output = 2; } -message GoogleInstanceIdentityAuth { +message InstanceIdentityAuth { string instance_id = 1; } // Agent represents a running agent on the workspace. message Agent { string id = 1; - map env = 2; - string startup_script = 3; + string name = 2; + map env = 3; + string startup_script = 4; + string operating_system = 5; + string architecture = 6; oneof auth { - string token = 4; - string instance_id = 5; + string token = 7; + string instance_id = 8; } } @@ -86,7 +89,7 @@ message Agent { message Resource { string name = 1; string type = 2; - Agent agent = 3; + repeated Agent agents = 3; } // Parse consumes source-code from a directory to produce inputs.