package provider import ( "context" "net/url" "regexp" "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" ) var ( // appSlugRegex is the regex used to validate the slug of a coder_app // resource. It must be a valid hostname and cannot contain two consecutive // hyphens or start/end with a hyphen. // // This regex is duplicated in the Coder source code, so make sure to update // it there as well. // // There are test cases for this regex in the Coder product. appSlugRegex = regexp.MustCompile(`^[a-z0-9](-?[a-z0-9])*$`) ) func appResource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, Description: "Use this resource to define shortcuts to access applications in a workspace.", CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { resourceData.SetId(uuid.NewString()) diags := diag.Diagnostics{} hiddenData := resourceData.Get("hidden") if hidden, ok := hiddenData.(bool); !ok { return diag.Errorf("hidden should be a bool") } else if hidden { if _, ok := resourceData.GetOk("display_name"); ok { diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "`display_name` set when app is hidden", }) } if _, ok := resourceData.GetOk("icon"); ok { diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "`icon` set when app is hidden", }) } if _, ok := resourceData.GetOk("order"); ok { diags = append(diags, diag.Diagnostic{ Severity: diag.Warning, Summary: "`order` set when app is hidden", }) } } return diags }, ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics { return nil }, DeleteContext: func(ctx context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics { return nil }, Schema: map[string]*schema.Schema{ "agent_id": { Type: schema.TypeString, Description: "The `id` property of a `coder_agent` resource to associate with.", ForceNew: true, Required: true, }, "command": { Type: schema.TypeString, Description: "A command to run in a terminal opening this app. In the web, " + "this will open in a new tab. In the CLI, this will SSH and execute the command. " + "Either `command` or `url` may be specified, but not both.", ConflictsWith: []string{"url"}, Optional: true, ForceNew: true, }, "icon": { Type: schema.TypeString, Description: "A URL to an icon that will display in the dashboard. View built-in " + "icons here: https://github.com/coder/coder/tree/main/site/static/icon. Use a " + "built-in icon with `\"${data.coder_workspace.me.access_url}/icon/<path>\"`.", ForceNew: true, Optional: true, ValidateFunc: func(i interface{}, s string) ([]string, []error) { _, err := url.Parse(s) if err != nil { return nil, []error{err} } return nil, nil }, }, "slug": { Type: schema.TypeString, Description: "A hostname-friendly name for the app. This is " + "used in URLs to access the app. May contain " + "alphanumerics and hyphens. Cannot start/end with a " + "hyphen or contain two consecutive hyphens.", ForceNew: true, Required: true, ValidateDiagFunc: func(val interface{}, c cty.Path) diag.Diagnostics { valStr, ok := val.(string) if !ok { return diag.Errorf("expected string, got %T", val) } if !appSlugRegex.MatchString(valStr) { return diag.Errorf(`invalid "coder_app" slug, must be a valid hostname (%q, cannot contain two consecutive hyphens or start/end with a hyphen): %q`, appSlugRegex.String(), valStr) } return nil }, }, "display_name": { Type: schema.TypeString, Description: "A display name to identify the app. Defaults to the slug.", ForceNew: true, Optional: true, }, "name": { Type: schema.TypeString, Description: "A display name to identify the app.", Deprecated: "`name` on apps is deprecated, use `display_name` instead", ForceNew: true, Optional: true, ConflictsWith: []string{"display_name"}, }, "subdomain": { Type: schema.TypeBool, Description: "Determines whether the app will be accessed via it's own " + "subdomain or whether it will be accessed via a path on Coder. If " + "wildcards have not been setup by the administrator then apps with " + "`subdomain` set to `true` will not be accessible. Defaults to `false`.", ForceNew: true, Optional: true, }, "relative_path": { Type: schema.TypeBool, Deprecated: "`relative_path` on apps is deprecated, use `subdomain` instead.", Description: "Specifies whether the URL will be accessed via a relative " + "path or wildcard. Use if wildcard routing is unavailable. Defaults to `true`.", ForceNew: true, Optional: true, ConflictsWith: []string{"subdomain"}, }, "share": { Type: schema.TypeString, Description: "Determines the level which the application " + "is shared at. Valid levels are `\"owner\"` (default), " + "`\"authenticated\"` and `\"public\"`. Level `\"owner\"` disables " + "sharing on the app, so only the workspace owner can " + "access it. Level `\"authenticated\"` shares the app with " + "all authenticated users. Level `\"public\"` shares it with " + "any user, including unauthenticated users. Permitted " + "application sharing levels can be configured site-wide " + "via a flag on `coder server` (Enterprise only).", ForceNew: true, Optional: true, Default: "owner", ValidateDiagFunc: func(val interface{}, c cty.Path) diag.Diagnostics { valStr, ok := val.(string) if !ok { return diag.Errorf("expected string, got %T", val) } switch valStr { case "owner", "authenticated", "public": return nil } return diag.Errorf("invalid app share %q, must be one of \"owner\", \"authenticated\", \"public\"", valStr) }, }, "url": { Type: schema.TypeString, Description: "An external url if `external=true` or a URL to be proxied to from inside the workspace. " + "This should be of the form `http://localhost:PORT[/SUBPATH]`. " + "Either `command` or `url` may be specified, but not both.", ForceNew: true, Optional: true, ConflictsWith: []string{"command"}, }, "external": { Type: schema.TypeBool, Description: "Specifies whether `url` is opened on the client machine " + "instead of proxied through the workspace.", Default: false, ForceNew: true, Optional: true, ConflictsWith: []string{"healthcheck", "command", "subdomain", "share"}, }, "healthcheck": { Type: schema.TypeSet, Description: "HTTP health checking to determine the application readiness.", ForceNew: true, Optional: true, MaxItems: 1, ConflictsWith: []string{"command"}, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ "url": { Type: schema.TypeString, Description: "HTTP address used determine the application readiness. A successful health check is a HTTP response code less than 500 returned before `healthcheck.interval` seconds.", ForceNew: true, Required: true, }, "interval": { Type: schema.TypeInt, Description: "Duration in seconds to wait between healthcheck requests.", ForceNew: true, Required: true, }, "threshold": { Type: schema.TypeInt, Description: "Number of consecutive heathcheck failures before returning an unhealthy status.", ForceNew: true, Required: true, }, }, }, }, "order": { Type: schema.TypeInt, Description: "The order determines the position of app in the UI presentation. The lowest order is shown first and apps with equal order are sorted by name (ascending order).", ForceNew: true, Optional: true, }, "hidden": { Type: schema.TypeBool, Description: "Determines if the app is visible in the UI (minimum coder version: v2.16).", Default: false, ForceNew: true, Optional: true, }, }, } }