feat: add support for coder_script (#9584)

* Add basic migrations

* Improve schema

* Refactor agent scripts into it's own package

* Support legacy start and stop script format

* Pipe the scripts!

* Finish the piping

* Fix context usage

* It works!

* Fix sql query

* Fix SQL query

* Rename `LogSourceID` -> `SourceID`

* Fix the FE

* fmt

* Rename migrations

* Fix log tests

* Fix lint err

* Fix gen

* Fix story type

* Rename source to script

* Fix schema jank

* Uncomment test

* Rename proto to TimeoutSeconds

* Fix comments

* Fix comments

* Fix legacy endpoint without specified log_source

* Fix non-blocking by default in agent

* Fix resources tests

* Fix dbfake

* Fix resources

* Fix linting I think

* Add fixtures

* fmt

* Fix startup script behavior

* Fix comments

* Fix context

* Fix cancel

* Fix SQL tests

* Fix e2e tests

* Interrupt on Windows

* Fix agent leaking script process

* Fix migrations

* Fix stories

* Fix duplicate logs appearing

* Gen

* Fix log location

* Fix tests

* Fix tests

* Fix log output

* Show display name in output

* Fix print

* Return timeout on start context

* Gen

* Fix fixture

* Fix the agent status

* Fix startup timeout msg

* Fix command using shared context

* Fix timeout draining

* Change signal type

* Add deterministic colors to startup script logs

---------

Co-authored-by: Muhammad Atif Ali <atif@coder.com>
This commit is contained in:
Kyle Carberry
2023-09-25 16:47:17 -05:00
committed by GitHub
parent dac1375880
commit 1262eef2c0
61 changed files with 3820 additions and 2117 deletions
+100 -35
View File
@@ -29,25 +29,26 @@ type agentMetadata struct {
// A mapping of attributes on the "coder_agent" resource.
type agentAttributes struct {
Auth string `mapstructure:"auth"`
OperatingSystem string `mapstructure:"os"`
Architecture string `mapstructure:"arch"`
Directory string `mapstructure:"dir"`
ID string `mapstructure:"id"`
Token string `mapstructure:"token"`
Env map[string]string `mapstructure:"env"`
StartupScript string `mapstructure:"startup_script"`
ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"`
TroubleshootingURL string `mapstructure:"troubleshooting_url"`
MOTDFile string `mapstructure:"motd_file"`
Auth string `mapstructure:"auth"`
OperatingSystem string `mapstructure:"os"`
Architecture string `mapstructure:"arch"`
Directory string `mapstructure:"dir"`
ID string `mapstructure:"id"`
Token string `mapstructure:"token"`
Env map[string]string `mapstructure:"env"`
// Deprecated, but remains here for backwards compatibility.
LoginBeforeReady bool `mapstructure:"login_before_ready"`
StartupScriptBehavior string `mapstructure:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"`
ShutdownScript string `mapstructure:"shutdown_script"`
ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"`
Metadata []agentMetadata `mapstructure:"metadata"`
DisplayApps []agentDisplayAppsAttributes `mapstructure:"display_apps"`
StartupScript string `mapstructure:"startup_script"`
StartupScriptBehavior string `mapstructure:"startup_script_behavior"`
StartupScriptTimeoutSeconds int32 `mapstructure:"startup_script_timeout"`
LoginBeforeReady bool `mapstructure:"login_before_ready"`
ShutdownScript string `mapstructure:"shutdown_script"`
ShutdownScriptTimeoutSeconds int32 `mapstructure:"shutdown_script_timeout"`
ConnectionTimeoutSeconds int32 `mapstructure:"connection_timeout"`
TroubleshootingURL string `mapstructure:"troubleshooting_url"`
MOTDFile string `mapstructure:"motd_file"`
Metadata []agentMetadata `mapstructure:"metadata"`
DisplayApps []agentDisplayAppsAttributes `mapstructure:"display_apps"`
}
type agentDisplayAppsAttributes struct {
@@ -76,6 +77,19 @@ type agentAppAttributes struct {
Healthcheck []appHealthcheckAttributes `mapstructure:"healthcheck"`
}
type agentScriptAttributes struct {
AgentID string `mapstructure:"agent_id"`
DisplayName string `mapstructure:"display_name"`
Icon string `mapstructure:"icon"`
Script string `mapstructure:"script"`
Cron string `mapstructure:"cron"`
LogPath string `mapstructure:"log_path"`
StartBlocksLogin bool `mapstructure:"start_blocks_login"`
RunOnStart bool `mapstructure:"run_on_start"`
RunOnStop bool `mapstructure:"run_on_stop"`
TimeoutSeconds int32 `mapstructure:"timeout"`
}
// A mapping of attributes on the "healthcheck" resource.
type appHealthcheckAttributes struct {
URL string `mapstructure:"url"`
@@ -107,6 +121,7 @@ type State struct {
// ConvertState consumes Terraform state and a GraphViz representation
// produced by `terraform graph` to produce resources consumable by Coder.
// nolint:gocognit // This function makes more sense being large for now, until refactored.
func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error) {
parsedGraph, err := gographviz.ParseString(rawGraph)
if err != nil {
@@ -206,22 +221,39 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
}
agent := &proto.Agent{
Name: tfResource.Name,
Id: attrs.ID,
Env: attrs.Env,
StartupScript: attrs.StartupScript,
OperatingSystem: attrs.OperatingSystem,
Architecture: attrs.Architecture,
Directory: attrs.Directory,
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
TroubleshootingUrl: attrs.TroubleshootingURL,
MotdFile: attrs.MOTDFile,
StartupScriptBehavior: startupScriptBehavior,
StartupScriptTimeoutSeconds: attrs.StartupScriptTimeoutSeconds,
ShutdownScript: attrs.ShutdownScript,
ShutdownScriptTimeoutSeconds: attrs.ShutdownScriptTimeoutSeconds,
Metadata: metadata,
DisplayApps: displayApps,
Name: tfResource.Name,
Id: attrs.ID,
Env: attrs.Env,
OperatingSystem: attrs.OperatingSystem,
Architecture: attrs.Architecture,
Directory: attrs.Directory,
ConnectionTimeoutSeconds: attrs.ConnectionTimeoutSeconds,
TroubleshootingUrl: attrs.TroubleshootingURL,
MotdFile: attrs.MOTDFile,
Metadata: metadata,
DisplayApps: displayApps,
}
// Support the legacy script attributes in the agent!
if attrs.StartupScript != "" {
agent.Scripts = append(agent.Scripts, &proto.Script{
// This is ▶️
Icon: "/emojis/25b6.png",
LogPath: "coder-startup-script.log",
DisplayName: "Startup Script",
Script: attrs.StartupScript,
StartBlocksLogin: startupScriptBehavior == string(codersdk.WorkspaceAgentStartupScriptBehaviorBlocking),
RunOnStart: true,
})
}
if attrs.ShutdownScript != "" {
agent.Scripts = append(agent.Scripts, &proto.Script{
// This is ◀️
Icon: "/emojis/25c0.png",
LogPath: "coder-shutdown-script.log",
DisplayName: "Shutdown Script",
Script: attrs.ShutdownScript,
RunOnStop: true,
})
}
switch attrs.Auth {
case "token":
@@ -403,6 +435,39 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
}
}
// Associate scripts with agents.
for _, resources := range tfResourcesByLabel {
for _, resource := range resources {
if resource.Type != "coder_script" {
continue
}
var attrs agentScriptAttributes
err = mapstructure.Decode(resource.AttributeValues, &attrs)
if err != nil {
return nil, xerrors.Errorf("decode app attributes: %w", err)
}
for _, agents := range resourceAgents {
for _, agent := range agents {
// Find agents with the matching ID and associate them!
if agent.Id != attrs.AgentID {
continue
}
agent.Scripts = append(agent.Scripts, &proto.Script{
DisplayName: attrs.DisplayName,
Icon: attrs.Icon,
Script: attrs.Script,
Cron: attrs.Cron,
LogPath: attrs.LogPath,
StartBlocksLogin: attrs.StartBlocksLogin,
RunOnStart: attrs.RunOnStart,
RunOnStop: attrs.RunOnStop,
TimeoutSeconds: attrs.TimeoutSeconds,
})
}
}
}
}
// Associate metadata blocks with resources.
resourceMetadata := map[string][]*proto.Resource_Metadata{}
resourceHidden := map[string]bool{}
@@ -482,7 +547,7 @@ func ConvertState(modules []*tfjson.StateModule, rawGraph string) (*State, error
if resource.Mode == tfjson.DataResourceMode {
continue
}
if resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
if resource.Type == "coder_script" || resource.Type == "coder_agent" || resource.Type == "coder_agent_instance" || resource.Type == "coder_app" || resource.Type == "coder_metadata" {
continue
}
label := convertAddressToLabel(resource.Address)
+77 -105
View File
@@ -54,7 +54,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -72,7 +71,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -91,7 +89,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_InstanceId{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -108,7 +105,6 @@ func TestConvertResources(t *testing.T) {
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -121,48 +117,42 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev1",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
StartupScriptBehavior: "non-blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev1",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}, {
Name: "dev2",
OperatingSystem: "darwin",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 1,
MotdFile: "/etc/motd",
StartupScriptBehavior: "non-blocking",
StartupScriptTimeoutSeconds: 30,
ShutdownScript: "echo bye bye",
ShutdownScriptTimeoutSeconds: 30,
DisplayApps: &displayApps,
Name: "dev2",
OperatingSystem: "darwin",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 1,
MotdFile: "/etc/motd",
DisplayApps: &displayApps,
Scripts: []*proto.Script{{
Icon: "/emojis/25c0.png",
DisplayName: "Shutdown Script",
RunOnStop: true,
LogPath: "coder-shutdown-script.log",
Script: "echo bye bye",
}},
}, {
Name: "dev3",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
TroubleshootingUrl: "https://coder.com/troubleshoot",
StartupScriptBehavior: "blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev3",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
TroubleshootingUrl: "https://coder.com/troubleshoot",
DisplayApps: &displayApps,
}, {
Name: "dev4",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
StartupScriptBehavior: "blocking",
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "dev4",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
},
@@ -199,7 +189,6 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -224,7 +213,6 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
@@ -263,11 +251,8 @@ func TestConvertResources(t *testing.T) {
Interval: 5,
Timeout: 1,
}},
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
},
@@ -307,7 +292,6 @@ func TestConvertResources(t *testing.T) {
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
StartupScript: " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log &\n",
Apps: []*proto.App{
{
Icon: "/icon/code.svg",
@@ -317,9 +301,15 @@ func TestConvertResources(t *testing.T) {
},
},
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Scripts: []*proto.Script{{
DisplayName: "Startup Script",
RunOnStart: true,
LogPath: "coder-startup-script.log",
Icon: "/emojis/25b6.png",
Script: " #!/bin/bash\n # home folder can be empty, so copying default bash settings\n if [ ! -f ~/.profile ]; then\n cp /etc/skel/.profile $HOME\n fi\n if [ ! -f ~/.bashrc ]; then\n cp /etc/skel/.bashrc $HOME\n fi\n # install and start code-server\n curl -fsSL https://code-server.dev/install.sh | sh | tee code-server-install.log\n code-server --auth none --port 13337 | tee code-server-install.log &\n",
}},
}},
},
},
@@ -329,15 +319,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@@ -411,15 +398,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@@ -440,15 +424,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "dev",
OperatingSystem: "windows",
ShutdownScriptTimeoutSeconds: 300,
StartupScriptTimeoutSeconds: 300,
Architecture: "arm64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
Name: "dev",
OperatingSystem: "windows",
Architecture: "arm64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
parameters: []*proto.RichParameter{{
@@ -496,15 +477,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &displayApps,
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &displayApps,
}},
}},
gitAuthProviders: []string{"github", "gitlab"},
@@ -514,14 +492,11 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &proto.DisplayApps{
VscodeInsiders: true,
WebTerminal: true,
@@ -534,15 +509,12 @@ func TestConvertResources(t *testing.T) {
Name: "dev",
Type: "null_resource",
Agents: []*proto.Agent{{
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
StartupScriptBehavior: "non-blocking",
ConnectionTimeoutSeconds: 120,
StartupScriptTimeoutSeconds: 300,
ShutdownScriptTimeoutSeconds: 300,
DisplayApps: &proto.DisplayApps{},
Name: "main",
OperatingSystem: "linux",
Architecture: "amd64",
Auth: &proto.Agent_Token{},
ConnectionTimeoutSeconds: 120,
DisplayApps: &proto.DisplayApps{},
}},
}},
},