Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat: Integrate workspace agent into coderd #398

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 5 additions & 10 deletions agent/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ type Options struct {
Logger slog.Logger
}

type Dialer func(ctx context.Context) (*peerbroker.Listener, error)
type Dialer func(ctx context.Context, options *peer.ConnOptions) (*peerbroker.Listener, error)

func New(dialer Dialer, options *Options) io.Closer {
func New(dialer Dialer, options *peer.ConnOptions) io.Closer {
ctx, cancelFunc := context.WithCancel(context.Background())
server := &server{
clientDialer: dialer,
Expand All @@ -75,7 +75,7 @@ func New(dialer Dialer, options *Options) io.Closer {

type server struct {
clientDialer Dialer
options *Options
options *peer.ConnOptions

closeCancel context.CancelFunc
closeMutex sync.Mutex
Expand Down Expand Up @@ -135,12 +135,7 @@ func (s *server) init(ctx context.Context) {
},
ServerConfigCallback: func(ctx ssh.Context) *gossh.ServerConfig {
return &gossh.ServerConfig{
Config: gossh.Config{
// "arcfour" is the fastest SSH cipher. We prioritize throughput
// over encryption here, because the WebRTC connection is already
// encrypted. If possible, we'd disable encryption entirely here.
Ciphers: []string{"arcfour"},
},
Config: gossh.Config{},
NoClientAuth: true,
}
},
Expand Down Expand Up @@ -249,7 +244,7 @@ func (s *server) run(ctx context.Context) {
// An exponential back-off occurs when the connection is failing to dial.
// This is to prevent server spam in case of a coderd outage.
for retrier := retry.New(50*time.Millisecond, 10*time.Second); retrier.Wait(ctx); {
peerListener, err = s.clientDialer(ctx)
peerListener, err = s.clientDialer(ctx, s.options)
if err != nil {
if errors.Is(err, context.Canceled) {
return
Expand Down
8 changes: 3 additions & 5 deletions agent/agent_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,9 @@ func TestAgent(t *testing.T) {

func setup(t *testing.T) proto.DRPCPeerBrokerClient {
client, server := provisionersdk.TransportPipe()
closer := agent.New(func(ctx context.Context) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, &peer.ConnOptions{
Logger: slogtest.Make(t, nil),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I understand correctly, it seems like the log level for this has changed - I wonder if that could be related to the test failure here: https://github.com/coder/coder/runs/5416391323?check_suite_focus=true#step:7:168

(thinking maybe this change increases the verbosity of logs, and one of those more-verbose logs is what gets emitted after the test is complete).

})
}, &agent.Options{
closer := agent.New(func(ctx context.Context, opts *peer.ConnOptions) (*peerbroker.Listener, error) {
return peerbroker.Listen(server, opts)
}, &peer.ConnOptions{
Logger: slogtest.Make(t, nil).Leveled(slog.LevelDebug),
})
t.Cleanup(func() {
Expand Down
15 changes: 8 additions & 7 deletions cli/projectcreate.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,6 @@ func projectCreate() *cobra.Command {
if err != nil {
return err
}
project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{
Name: name,
VersionImportJobID: job.ID,
})
if err != nil {
return err
}

_, err = prompt(cmd, &promptui.Prompt{
Label: "Create project?",
Expand All @@ -91,6 +84,14 @@ func projectCreate() *cobra.Command {
return err
}

project, err := client.CreateProject(cmd.Context(), organization.Name, coderd.CreateProjectRequest{
Name: name,
VersionImportJobID: job.ID,
})
if err != nil {
return err
}

_, _ = fmt.Fprintf(cmd.OutOrStdout(), "%s The %s project has been created!\n", caret, color.HiCyanString(project.Name))
_, err = prompt(cmd, &promptui.Prompt{
Label: "Create a new workspace?",
Expand Down
1 change: 1 addition & 0 deletions cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func Root() *cobra.Command {
cmd.AddCommand(projects())
cmd.AddCommand(workspaces())
cmd.AddCommand(users())
cmd.AddCommand(ssh())

cmd.PersistentFlags().String(varGlobalConfig, configdir.LocalConfig("coder"), "Path to the global `coder` config directory")
cmd.PersistentFlags().Bool(varForceTty, false, "Force the `coder` command to run as if connected to a TTY")
Expand Down
64 changes: 64 additions & 0 deletions cli/ssh.go
Original file line number Diff line number Diff line change
@@ -1 +1,65 @@
package cli

import (
"io"

"github.com/coder/coder/agent"
"github.com/coder/coder/peer"
"github.com/coder/coder/peerbroker"
"github.com/pion/webrtc/v3"
"github.com/spf13/cobra"
)

func ssh() *cobra.Command {
return &cobra.Command{
Use: "ssh",
RunE: func(cmd *cobra.Command, args []string) error {
client, err := createClient(cmd)
if err != nil {
return err
}
organization, err := currentOrganization(cmd, client)
if err != nil {
return err
}
history, err := client.WorkspaceHistory(cmd.Context(), "", "kyle", "")
if err != nil {
return err
}
resources, err := client.WorkspaceProvisionJobResources(cmd.Context(), organization.Name, history.ProvisionJobID)
if err != nil {
return err
}
for _, resource := range resources {
if resource.Agent == nil {
continue
}
wagent, err := client.WorkspaceAgentConnect(cmd.Context(), organization.Name, history.ProvisionJobID, resource.ID)
if err != nil {
return err
}
stream, err := wagent.NegotiateConnection(cmd.Context())
if err != nil {
return err
}
conn, err := peerbroker.Dial(stream, []webrtc.ICEServer{{
URLs: []string{"stun:stun.l.google.com:19302"},
}}, &peer.ConnOptions{
// Logger: slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug),
})
if err != nil {
return err
}
sshConn, err := agent.DialSSH(conn)
if err != nil {
return err
}
go func() {
_, _ = io.Copy(cmd.OutOrStdout(), sshConn)
}()
_, _ = io.Copy(sshConn, cmd.InOrStdin())
}
return nil
},
}
}
62 changes: 62 additions & 0 deletions cli/workspaceagent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package cli

import (
"net/url"
"os"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
"github.com/powersj/whatsthis/pkg/cloud"
"github.com/spf13/cobra"
"golang.org/x/xerrors"

"github.com/coder/coder/agent"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/peer"
)

func workspaceAgent() *cobra.Command {
return &cobra.Command{
Use: "agent",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having the coder agent command is a huge step!

// This command isn't useful for users, and seems
// more likely to confuse.
Hidden: true,
Comment on lines +21 to +23
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me 👍

RunE: func(cmd *cobra.Command, args []string) error {
coderURLRaw, exists := os.LookupEnv("CODER_URL")
if !exists {
return xerrors.New("CODER_URL must be set")
}
coderURL, err := url.Parse(coderURLRaw)
if err != nil {
return xerrors.Errorf("parse %q: %w", coderURLRaw, err)
}
client := codersdk.New(coderURL)
sessionToken, exists := os.LookupEnv("CODER_TOKEN")
if !exists {
probe, err := cloud.New()
if err != nil {
return xerrors.Errorf("probe cloud: %w", err)
}
if !probe.Detected {
return xerrors.Errorf("no valid authentication method found; set \"CODER_TOKEN\"")
}
switch {
case probe.GCP():
response, err := client.AuthenticateWorkspaceAgentUsingGoogleCloudIdentity(cmd.Context(), "", nil)
if err != nil {
return xerrors.Errorf("authenticate workspace with gcp: %w", err)
}
sessionToken = response.SessionToken
default:
return xerrors.Errorf("%q authentication not supported; set \"CODER_TOKEN\" instead", probe.Name)
}
}
client.SessionToken = sessionToken
closer := agent.New(client.WorkspaceAgentServe, &peer.ConnOptions{
Logger: slog.Make(sloghuman.Sink(cmd.OutOrStdout())).Leveled(slog.LevelDebug),
})
<-cmd.Context().Done()
return closer.Close()
},
}
}
1 change: 1 addition & 0 deletions cli/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ func workspaces() *cobra.Command {
cmd := &cobra.Command{
Use: "workspaces",
}
cmd.AddCommand(workspaceAgent())
cmd.AddCommand(workspaceCreate())

return cmd
Expand Down
20 changes: 13 additions & 7 deletions coderd/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/spf13/cobra"
"golang.org/x/xerrors"
"google.golang.org/api/idtoken"

"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
Expand All @@ -34,14 +35,19 @@ func Root() *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error {
logger := slog.Make(sloghuman.Sink(os.Stderr))
accessURL := &url.URL{
Scheme: "http",
Host: address,
Scheme: "https",
Host: "momentum-finals-representation-failed.trycloudflare.com",
}
googleTokenValidator, err := idtoken.NewValidator(cmd.Context())
if err != nil {
return xerrors.Errorf("create google token validator: %w", err)
}
handler, closeCoderd := coderd.New(&coderd.Options{
AccessURL: accessURL,
Logger: logger,
Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(),
AccessURL: accessURL,
Logger: logger,
Database: databasefake.New(),
Pubsub: database.NewPubsubInMemory(),
GoogleTokenValidator: googleTokenValidator,
})

listener, err := net.Listen("tcp", address)
Expand Down Expand Up @@ -98,7 +104,7 @@ func newProvisionerDaemon(ctx context.Context, client *codersdk.Client, logger s
if err != nil {
return nil, err
}
return provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
return provisionerd.New(client.ProvisionerDaemonServe, &provisionerd.Options{
Logger: logger,
PollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
Expand Down
13 changes: 12 additions & 1 deletion coderd/coderd.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ func New(options *Options) (http.Handler, func()) {
r.Route("/authenticate", func(r chi.Router) {
r.Post("/google-instance-identity", api.postAuthenticateWorkspaceAgentUsingGoogleInstanceIdentity)
})
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceAgent(options.Database))
r.Get("/serve", api.workspaceAgentServe)
})
})

r.Route("/upload", func(r chi.Router) {
Expand All @@ -134,7 +138,7 @@ func New(options *Options) (http.Handler, func()) {
r.Get("/", api.provisionerJobByID)
r.Get("/schemas", api.projectImportJobSchemasByID)
r.Get("/parameters", api.projectImportJobParametersByID)
r.Get("/resources", api.projectImportJobResourcesByID)
r.Get("/resources", api.provisionerJobResourcesByID)
r.Get("/logs", api.provisionerJobLogsByID)
})
})
Expand All @@ -148,6 +152,13 @@ func New(options *Options) (http.Handler, func()) {
r.Use(httpmw.ExtractProvisionerJobParam(options.Database))
r.Get("/", api.provisionerJobByID)
r.Get("/logs", api.provisionerJobLogsByID)
r.Route("/resources", func(r chi.Router) {
r.Get("/", api.provisionerJobResourcesByID)
r.Route("/{workspaceresource}", func(r chi.Router) {
r.Use(httpmw.ExtractWorkspaceResourceParam(options.Database))
r.Get("/agent", api.workspaceAgentConnectByResource)
})
})
})
})

Expand Down
2 changes: 1 addition & 1 deletion coderd/coderdtest/coderdtest.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func NewProvisionerDaemon(t *testing.T, client *codersdk.Client) io.Closer {
require.NoError(t, err)
}()

closer := provisionerd.New(client.ProvisionerDaemonClient, &provisionerd.Options{
closer := provisionerd.New(client.ProvisionerDaemonServe, &provisionerd.Options{
Logger: slogtest.Make(t, nil).Named("provisionerd").Leveled(slog.LevelDebug),
PollInterval: 50 * time.Millisecond,
UpdateInterval: 50 * time.Millisecond,
Expand Down
26 changes: 0 additions & 26 deletions coderd/projectimport.go
Original file line number Diff line number Diff line change
Expand Up @@ -154,29 +154,3 @@ func (api *api) projectImportJobParametersByID(rw http.ResponseWriter, r *http.R
render.Status(r, http.StatusOK)
render.JSON(rw, r, values)
}

// Returns resources for an import job by ID.
func (api *api) projectImportJobResourcesByID(rw http.ResponseWriter, r *http.Request) {
job := httpmw.ProvisionerJobParam(r)
if !convertProvisionerJob(job).Status.Completed() {
httpapi.Write(rw, http.StatusPreconditionFailed, httpapi.Response{
Message: "Job hasn't completed!",
})
return
}
resources, err := api.Database.GetProvisionerJobResourcesByJobID(r.Context(), job.ID)
if errors.Is(err, sql.ErrNoRows) {
err = nil
}
if err != nil {
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
Message: fmt.Sprintf("get project import job resources: %s", err),
})
return
}
if resources == nil {
resources = []database.ProvisionerJobResource{}
}
render.Status(r, http.StatusOK)
render.JSON(rw, r, resources)
}
9 changes: 8 additions & 1 deletion coderd/provisionerdaemons.go
Original file line number Diff line number Diff line change
Expand Up @@ -590,12 +590,19 @@ func insertProvisionerJobResource(ctx context.Context, db database.Store, jobID
Valid: true,
}
}
authToken := uuid.New()
if protoResource.Agent.GetToken() != "" {
authToken, err = uuid.Parse(protoResource.Agent.GetToken())
if err != nil {
return xerrors.Errorf("invalid auth token format; must be uuid: %w", err)
}
}

_, err := db.InsertProvisionerJobAgent(ctx, database.InsertProvisionerJobAgentParams{
ID: resource.AgentID.UUID,
CreatedAt: database.Now(),
ResourceID: resource.ID,
AuthToken: uuid.New(),
AuthToken: authToken,
AuthInstanceID: instanceID,
EnvironmentVariables: env,
StartupScript: sql.NullString{
Expand Down
Loading