2022-03-07 17:40:54 +00:00
package cli
import (
2022-03-25 16:34:45 +00:00
"context"
2022-06-17 05:54:45 +00:00
"fmt"
2022-03-28 19:31:03 +00:00
"net/http"
2022-06-06 13:38:33 +00:00
_ "net/http/pprof" //nolint: gosec
2022-03-07 17:40:54 +00:00
"net/url"
2022-05-02 16:36:51 +00:00
"os"
"path/filepath"
2022-06-17 16:51:46 +00:00
"runtime"
2022-03-25 16:34:45 +00:00
"time"
2022-03-07 17:40:54 +00:00
2022-03-25 19:48:08 +00:00
"cloud.google.com/go/compute/metadata"
2022-03-07 17:40:54 +00:00
"github.com/spf13/cobra"
"golang.org/x/xerrors"
2022-03-22 19:17:50 +00:00
"cdr.dev/slog"
"cdr.dev/slog/sloggers/sloghuman"
2022-03-07 17:40:54 +00:00
"github.com/coder/coder/agent"
2022-06-17 16:51:46 +00:00
"github.com/coder/coder/agent/reaper"
2022-03-28 19:26:41 +00:00
"github.com/coder/coder/cli/cliflag"
2022-03-07 17:40:54 +00:00
"github.com/coder/coder/codersdk"
2022-03-25 16:34:45 +00:00
"github.com/coder/retry"
2022-05-02 16:36:51 +00:00
"gopkg.in/natefinch/lumberjack.v2"
2022-03-07 17:40:54 +00:00
)
func workspaceAgent ( ) * cobra . Command {
2022-03-25 19:48:08 +00:00
var (
2022-06-06 13:38:33 +00:00
auth string
pprofEnabled bool
pprofAddress string
2022-06-21 23:01:34 +00:00
noReap bool
2022-03-25 19:48:08 +00:00
)
cmd := & cobra . Command {
2022-03-07 17:40:54 +00:00
Use : "agent" ,
2022-03-25 16:34:45 +00:00
// This command isn't useful to manually execute.
2022-03-07 17:40:54 +00:00
Hidden : true ,
RunE : func ( cmd * cobra . Command , args [ ] string ) error {
2022-04-30 16:40:30 +00:00
rawURL , err := cmd . Flags ( ) . GetString ( varAgentURL )
if err != nil {
return xerrors . Errorf ( "CODER_AGENT_URL must be set: %w" , err )
2022-03-07 17:40:54 +00:00
}
2022-03-25 19:48:08 +00:00
coderURL , err := url . Parse ( rawURL )
2022-03-07 17:40:54 +00:00
if err != nil {
2022-03-25 19:48:08 +00:00
return xerrors . Errorf ( "parse %q: %w" , rawURL , err )
2022-03-07 17:40:54 +00:00
}
2022-05-02 16:36:51 +00:00
logWriter := & lumberjack . Logger {
Filename : filepath . Join ( os . TempDir ( ) , "coder-agent.log" ) ,
MaxSize : 5 , // MB
}
defer logWriter . Close ( )
logger := slog . Make ( sloghuman . Sink ( cmd . ErrOrStderr ( ) ) , sloghuman . Sink ( logWriter ) ) . Leveled ( slog . LevelDebug )
2022-06-17 16:51:46 +00:00
isLinux := runtime . GOOS == "linux"
// Spawn a reaper so that we don't accumulate a ton
// of zombie processes.
2022-06-21 23:01:34 +00:00
if reaper . IsInitProcess ( ) && ! noReap && isLinux {
2022-06-17 16:51:46 +00:00
logger . Info ( cmd . Context ( ) , "spawning reaper process" )
2022-06-21 23:01:34 +00:00
// Do not start a reaper on the child process. It's important
// to do this else we fork bomb ourselves.
args := append ( os . Args , "--no-reap" )
err := reaper . ForkReap ( reaper . WithExecArgs ( args ... ) )
2022-06-17 16:51:46 +00:00
if err != nil {
logger . Error ( cmd . Context ( ) , "failed to reap" , slog . Error ( err ) )
return xerrors . Errorf ( "fork reap: %w" , err )
}
logger . Info ( cmd . Context ( ) , "reaper process exiting" )
return nil
}
2022-03-07 17:40:54 +00:00
client := codersdk . New ( coderURL )
2022-03-28 19:31:03 +00:00
2022-06-06 13:38:33 +00:00
if pprofEnabled {
srvClose := serveHandler ( cmd . Context ( ) , logger , nil , pprofAddress , "pprof" )
defer srvClose ( )
} else {
// If pprof wasn't enabled at startup, allow a
// `kill -USR1 $agent_pid` to start it (on Unix).
srvClose := agentStartPPROFOnUSR1 ( cmd . Context ( ) , logger , pprofAddress )
defer srvClose ( )
}
2022-03-28 19:31:03 +00:00
// exchangeToken returns a session token.
// This is abstracted to allow for the same looping condition
// regardless of instance identity auth type.
var exchangeToken func ( context . Context ) ( codersdk . WorkspaceAgentAuthenticateResponse , error )
2022-03-25 16:34:45 +00:00
switch auth {
case "token" :
2022-04-30 16:40:30 +00:00
token , err := cmd . Flags ( ) . GetString ( varAgentToken )
if err != nil {
return xerrors . Errorf ( "CODER_AGENT_TOKEN must be set for token auth: %w" , err )
2022-03-25 16:34:45 +00:00
}
2022-03-28 19:26:41 +00:00
client . SessionToken = token
2022-03-25 16:34:45 +00:00
case "google-instance-identity" :
2022-03-25 19:48:08 +00:00
// This is *only* done for testing to mock client authentication.
// This will never be set in a production scenario.
var gcpClient * metadata . Client
gcpClientRaw := cmd . Context ( ) . Value ( "gcp-client" )
if gcpClientRaw != nil {
gcpClient , _ = gcpClientRaw . ( * metadata . Client )
}
2022-03-28 19:31:03 +00:00
exchangeToken = func ( ctx context . Context ) ( codersdk . WorkspaceAgentAuthenticateResponse , error ) {
return client . AuthWorkspaceGoogleInstanceIdentity ( ctx , "" , gcpClient )
}
case "aws-instance-identity" :
// This is *only* done for testing to mock client authentication.
// This will never be set in a production scenario.
var awsClient * http . Client
awsClientRaw := cmd . Context ( ) . Value ( "aws-client" )
if awsClientRaw != nil {
awsClient , _ = awsClientRaw . ( * http . Client )
if awsClient != nil {
client . HTTPClient = awsClient
}
}
exchangeToken = func ( ctx context . Context ) ( codersdk . WorkspaceAgentAuthenticateResponse , error ) {
return client . AuthWorkspaceAWSInstanceIdentity ( ctx )
}
case "azure-instance-identity" :
2022-04-19 13:48:13 +00:00
// This is *only* done for testing to mock client authentication.
// This will never be set in a production scenario.
var azureClient * http . Client
azureClientRaw := cmd . Context ( ) . Value ( "azure-client" )
if azureClientRaw != nil {
azureClient , _ = azureClientRaw . ( * http . Client )
if azureClient != nil {
client . HTTPClient = azureClient
}
}
exchangeToken = func ( ctx context . Context ) ( codersdk . WorkspaceAgentAuthenticateResponse , error ) {
return client . AuthWorkspaceAzureInstanceIdentity ( ctx )
}
2022-03-28 19:31:03 +00:00
}
2022-03-25 19:48:08 +00:00
2022-03-28 19:31:03 +00:00
if exchangeToken != nil {
// Agent's can start before resources are returned from the provisioner
// daemon. If there are many resources being provisioned, this time
// could be significant. This is arbitrarily set at an hour to prevent
// tons of idle agents from pinging coderd.
ctx , cancelFunc := context . WithTimeout ( cmd . Context ( ) , time . Hour )
2022-03-25 16:34:45 +00:00
defer cancelFunc ( )
for retry . New ( 100 * time . Millisecond , 5 * time . Second ) . Wait ( ctx ) {
var response codersdk . WorkspaceAgentAuthenticateResponse
2022-03-25 19:48:08 +00:00
2022-03-28 19:31:03 +00:00
response , err = exchangeToken ( ctx )
2022-03-25 16:34:45 +00:00
if err != nil {
2022-03-28 19:31:03 +00:00
logger . Warn ( ctx , "authenticate workspace" , slog . F ( "method" , auth ) , slog . Error ( err ) )
2022-03-25 16:34:45 +00:00
continue
}
client . SessionToken = response . SessionToken
2022-03-28 19:31:03 +00:00
logger . Info ( ctx , "authenticated" , slog . F ( "method" , auth ) )
2022-03-25 16:34:45 +00:00
break
}
2022-03-07 17:40:54 +00:00
if err != nil {
2022-03-25 16:34:45 +00:00
return xerrors . Errorf ( "agent failed to authenticate in time: %w" , err )
2022-03-07 17:40:54 +00:00
}
}
2022-03-28 19:31:03 +00:00
2022-06-17 05:54:45 +00:00
executablePath , err := os . Executable ( )
if err != nil {
return xerrors . Errorf ( "getting os executable: %w" , err )
}
err = os . Setenv ( "PATH" , fmt . Sprintf ( "%s%c%s" , os . Getenv ( "PATH" ) , filepath . ListSeparator , filepath . Dir ( executablePath ) ) )
if err != nil {
return xerrors . Errorf ( "add executable to $PATH: %w" , err )
}
2022-04-29 22:30:10 +00:00
closer := agent . New ( client . ListenWorkspaceAgent , & agent . Options {
Logger : logger ,
2022-04-30 16:40:30 +00:00
EnvironmentVariables : map [ string ] string {
// Override the "CODER_AGENT_TOKEN" variable in all
// shells so "gitssh" works!
"CODER_AGENT_TOKEN" : client . SessionToken ,
} ,
2022-04-29 22:30:10 +00:00
} )
2022-03-07 17:40:54 +00:00
<- cmd . Context ( ) . Done ( )
return closer . Close ( )
} ,
}
2022-03-28 19:26:41 +00:00
2022-04-30 16:40:30 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & auth , "auth" , "" , "CODER_AGENT_AUTH" , "token" , "Specify the authentication type to use for the agent" )
2022-06-06 13:38:33 +00:00
cliflag . BoolVarP ( cmd . Flags ( ) , & pprofEnabled , "pprof-enable" , "" , "CODER_AGENT_PPROF_ENABLE" , false , "Enable serving pprof metrics on the address defined by --pprof-address." )
2022-06-21 23:01:34 +00:00
cliflag . BoolVarP ( cmd . Flags ( ) , & noReap , "no-reap" , "" , "" , false , "Do not start a process reaper." )
2022-06-06 13:38:33 +00:00
cliflag . StringVarP ( cmd . Flags ( ) , & pprofAddress , "pprof-address" , "" , "CODER_AGENT_PPROF_ADDRESS" , "127.0.0.1:6060" , "The address to serve pprof." )
2022-03-25 19:48:08 +00:00
return cmd
2022-03-07 17:40:54 +00:00
}