Skip to content

feat: add resources_monitoring field to agent #331

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

Merged
merged 12 commits into from
Feb 7, 2025
4 changes: 2 additions & 2 deletions docs/resources/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ resource "kubernetes_pod" "dev" {
- `metadata` (Block List) Each `metadata` block defines a single item consisting of a key/value pair. This feature is in alpha and may break in future releases. (see [below for nested schema](#nestedblock--metadata))
- `motd_file` (String) The path to a file within the workspace containing a message to display to users when they login via SSH. A typical value would be `"/etc/motd"`.
- `order` (Number) The order determines the position of agents in the UI presentation. The lowest order is shown first and agents with equal order are sorted by name (ascending order).
- `resources_monitoring` (Block Set) The resources monitoring configuration for this agent. (see [below for nested schema](#nestedblock--resources_monitoring))
- `resources_monitoring` (Block Set, Max: 1) The resources monitoring configuration for this agent. (see [below for nested schema](#nestedblock--resources_monitoring))
- `shutdown_script` (String) A script to run before the agent is stopped. The script should exit when it is done to signal that the workspace can be stopped. This option is an alias for defining a `coder_script` resource with `run_on_stop` set to `true`.
- `startup_script` (String) A script to run after the agent starts. The script should exit when it is done to signal that the agent is ready. This option is an alias for defining a `coder_script` resource with `run_on_start` set to `true`.
- `startup_script_behavior` (String) This option sets the behavior of the `startup_script`. When set to `"blocking"`, the `startup_script` must exit before the workspace is ready. When set to `"non-blocking"`, the `startup_script` may run in the background and the workspace will be ready immediately. Default is `"non-blocking"`, although `"blocking"` is recommended. This option is an alias for defining a `coder_script` resource with `start_blocks_login` set to `true` (blocking).
Expand Down Expand Up @@ -124,7 +124,7 @@ Optional:

Optional:

- `memory` (Block Set) The memory monitoring configuration for this agent. (see [below for nested schema](#nestedblock--resources_monitoring--memory))
- `memory` (Block Set, Max: 1) The memory monitoring configuration for this agent. (see [below for nested schema](#nestedblock--resources_monitoring--memory))
- `volume` (Block Set) The volumes monitoring configuration for this agent. (see [below for nested schema](#nestedblock--resources_monitoring--volume))

<a id="nestedblock--resources_monitoring--memory"></a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ data "coder_workspace" "dev" {}
resource "coder_agent" "main" {
arch = data.coder_provisioner.dev.arch
os = data.coder_provisioner.dev.os
dir = "/workspace"
resources_monitoring {
memory {
enabled = true
Expand Down
89 changes: 74 additions & 15 deletions provider/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package provider
import (
"context"
"fmt"
"path/filepath"
"reflect"
"strings"

"github.com/google/uuid"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
Expand Down Expand Up @@ -264,13 +266,15 @@ func agentResource() *schema.Resource {
Description: "The resources monitoring configuration for this agent.",
ForceNew: true,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"memory": {
Type: schema.TypeSet,
Description: "The memory monitoring configuration for this agent.",
ForceNew: true,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"enabled": {
Expand All @@ -284,6 +288,12 @@ func agentResource() *schema.Resource {
Description: "The memory usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.",
ForceNew: true,
Required: true,
ValidateDiagFunc: func(i interface{}, s cty.Path) diag.Diagnostics {
if i.(int) < 0 || i.(int) > 100 {
return diag.Errorf("volume threshold must be between 0 and 100")
}
return nil
},
},
},
},
Expand All @@ -300,6 +310,17 @@ func agentResource() *schema.Resource {
Description: "The path of the volume to monitor.",
ForceNew: true,
Required: true,
ValidateDiagFunc: func(i interface{}, s cty.Path) diag.Diagnostics {
if i.(string) == "" {
return diag.Errorf("volume path must not be empty")
}

if !filepath.IsAbs(i.(string)) {
return diag.Errorf("volume path must be an absolute path")
}

return nil
},
},
"enabled": {
Type: schema.TypeBool,
Expand All @@ -312,6 +333,12 @@ func agentResource() *schema.Resource {
Description: "The volume usage threshold in percentage at which to trigger an alert. Value should be between 0 and 100.",
ForceNew: true,
Required: true,
ValidateDiagFunc: func(i interface{}, s cty.Path) diag.Diagnostics {
if i.(int) < 0 || i.(int) > 100 {
return diag.Errorf("volume threshold must be between 0 and 100")
}
return nil
},
},
},
},
Expand All @@ -321,29 +348,61 @@ func agentResource() *schema.Resource {
},
},
CustomizeDiff: func(ctx context.Context, rd *schema.ResourceDiff, i any) error {
if !rd.HasChange("metadata") {
return nil
if rd.HasChange("metadata") {
keys := map[string]bool{}
metadata, ok := rd.Get("metadata").([]any)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata, expected []any", rd.Get("metadata"))
}
for _, t := range metadata {
obj, ok := t.(map[string]any)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata, expected map[string]any", t)
}
key, ok := obj["key"].(string)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata key, expected string", obj["key"])
}
if keys[key] {
return xerrors.Errorf("duplicate agent metadata key %q", key)
}
keys[key] = true
}
}

keys := map[string]bool{}
metadata, ok := rd.Get("metadata").([]any)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata, expected []any", rd.Get("metadata"))
}
for _, t := range metadata {
obj, ok := t.(map[string]any)
if rd.HasChange("resources_monitoring") {
monitors, ok := rd.Get("resources_monitoring").(*schema.Set)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata, expected map[string]any", t)
return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", rd.Get("resources_monitoring.0.volume"))
}
key, ok := obj["key"].(string)

monitor := monitors.List()[0].(map[string]any)

volumes, ok := monitor["volume"].(*schema.Set)
if !ok {
return xerrors.Errorf("unexpected type %T for metadata key, expected string", obj["key"])
return xerrors.Errorf("unexpected type %T for resources_monitoring.0.volume, expected []any", monitor["volume"])
}
if keys[key] {
return xerrors.Errorf("duplicate agent metadata key %q", key)

paths := map[string]bool{}
for _, volume := range volumes.List() {
obj, ok := volume.(map[string]any)
if !ok {
return xerrors.Errorf("unexpected type %T for volume, expected map[string]any", volume)
}

// print path for debug purpose

path, ok := obj["path"].(string)
if !ok {
return xerrors.Errorf("unexpected type %T for volume path, expected string", obj["path"])
}
if paths[path] {
return xerrors.Errorf("duplicate volume path %q", path)
}
paths[path] = true
}
keys[key] = true
}

return nil
},
}
Expand Down
Loading
Loading