Skip to content

chore: Split up the provider file #55

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 1 commit into from
Sep 28, 2022
Merged
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
161 changes: 161 additions & 0 deletions internal/provider/agent.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
package provider

import (
"context"
"fmt"
"os"
"reflect"
"strings"

"github.com/google/uuid"
"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"
)

func agentResource() *schema.Resource {
return &schema.Resource{
Description: "Use this resource to associate an agent.",
CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
// This should be a real authentication token!
resourceData.SetId(uuid.NewString())
err := resourceData.Set("token", uuid.NewString())
if err != nil {
return diag.FromErr(err)
}
return updateInitScript(resourceData, i)
},
ReadWithoutTimeout: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
err := resourceData.Set("token", uuid.NewString())
if err != nil {
return diag.FromErr(err)
}
return updateInitScript(resourceData, i)
},
DeleteContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
return nil
},
Schema: map[string]*schema.Schema{
"init_script": {
Type: schema.TypeString,
Computed: true,
Description: "Run this script on startup of an instance to initialize the agent.",
},
"arch": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
Description: `The architecture the agent will run on. Must be one of: "amd64", "armv7", "arm64".`,
ValidateFunc: validation.StringInSlice([]string{"amd64", "armv7", "arm64"}, false),
},
"auth": {
Type: schema.TypeString,
Default: "token",
ForceNew: true,
Optional: true,
Description: `The authentication type the agent will use. Must be one of: "token", "google-instance-identity", "aws-instance-identity", "azure-instance-identity".`,
ValidateFunc: validation.StringInSlice([]string{"token", "google-instance-identity", "aws-instance-identity", "azure-instance-identity"}, false),
},
"dir": {
Type: schema.TypeString,
ForceNew: true,
Optional: true,
Description: "The starting directory when a user creates a shell session. Defaults to $HOME.",
},
"env": {
ForceNew: true,
Description: "A mapping of environment variables to set inside the workspace.",
Type: schema.TypeMap,
Optional: true,
},
"os": {
Type: schema.TypeString,
ForceNew: true,
Required: true,
Description: `The operating system the agent will run on. Must be one of: "linux", "darwin", or "windows".`,
ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false),
},
"startup_script": {
ForceNew: true,
Description: "A script to run after the agent starts.",
Type: schema.TypeString,
Optional: true,
},
"token": {
ForceNew: true,
Sensitive: true,
Description: `Set the environment variable "CODER_AGENT_TOKEN" with this token to authenticate an agent.`,
Type: schema.TypeString,
Computed: true,
},
},
}
}

func agentInstanceResource() *schema.Resource {
return &schema.Resource{
Description: "Use this resource to associate an instance ID with an agent for zero-trust " +
"authentication. This association is done automatically for \"google_compute_instance\", " +
"\"aws_instance\", \"azurerm_linux_virtual_machine\", and " +
"\"azurerm_windows_virtual_machine\" resources.",
CreateContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
resourceData.SetId(uuid.NewString())
return nil
},
ReadContext: func(c context.Context, resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
return nil
},
DeleteContext: func(c 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,
},
"instance_id": {
ForceNew: true,
Required: true,
Description: `The instance identifier of a provisioned resource.`,
Type: schema.TypeString,
},
},
}
}

// updateInitScript fetches parameters from a "coder_agent" to produce the
// agent script from environment variables.
func updateInitScript(resourceData *schema.ResourceData, i interface{}) diag.Diagnostics {
config, valid := i.(config)
if !valid {
return diag.Errorf("config was unexpected type %q", reflect.TypeOf(i).String())
}
auth, valid := resourceData.Get("auth").(string)
if !valid {
return diag.Errorf("auth was unexpected type %q", reflect.TypeOf(resourceData.Get("auth")))
}
operatingSystem, valid := resourceData.Get("os").(string)
if !valid {
return diag.Errorf("os was unexpected type %q", reflect.TypeOf(resourceData.Get("os")))
}
arch, valid := resourceData.Get("arch").(string)
if !valid {
return diag.Errorf("arch was unexpected type %q", reflect.TypeOf(resourceData.Get("arch")))
}
accessURL, err := config.URL.Parse("/")
if err != nil {
return diag.Errorf("parse access url: %s", err)
}
script := os.Getenv(fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", operatingSystem, arch))
if script != "" {
script = strings.ReplaceAll(script, "${ACCESS_URL}", accessURL.String())
script = strings.ReplaceAll(script, "${AUTH_TYPE}", auth)
}
err = resourceData.Set("init_script", script)
if err != nil {
return diag.FromErr(err)
}
return nil
}
100 changes: 100 additions & 0 deletions internal/provider/agent_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package provider_test

import (
"testing"

"github.com/coder/terraform-provider-coder/internal/provider"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/require"
)

func TestAgent(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
Providers: map[string]*schema.Provider{
"coder": provider.New(),
},
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: `
provider "coder" {
url = "https://example.com"
}
resource "coder_agent" "new" {
os = "linux"
arch = "amd64"
auth = "aws-instance-identity"
dir = "/tmp"
env = {
hi = "test"
}
startup_script = "echo test"
}
`,
Check: func(state *terraform.State) error {
require.Len(t, state.Modules, 1)
require.Len(t, state.Modules[0].Resources, 1)
resource := state.Modules[0].Resources["coder_agent.new"]
require.NotNil(t, resource)
for _, key := range []string{
"token",
"os",
"arch",
"auth",
"dir",
"env.hi",
"startup_script",
} {
value := resource.Primary.Attributes[key]
t.Logf("%q = %q", key, value)
require.NotNil(t, value)
require.Greater(t, len(value), 0)
}
return nil
},
}},
})
}

func TestAgentInstance(t *testing.T) {
t.Parallel()
resource.Test(t, resource.TestCase{
Providers: map[string]*schema.Provider{
"coder": provider.New(),
},
IsUnitTest: true,
Steps: []resource.TestStep{{
Config: `
provider "coder" {
url = "https://example.com"
}
resource "coder_agent" "dev" {
os = "linux"
arch = "amd64"
}
resource "coder_agent_instance" "new" {
agent_id = coder_agent.dev.id
instance_id = "hello"
}
`,
Check: func(state *terraform.State) error {
require.Len(t, state.Modules, 1)
require.Len(t, state.Modules[0].Resources, 2)
resource := state.Modules[0].Resources["coder_agent_instance.new"]
require.NotNil(t, resource)
for _, key := range []string{
"agent_id",
"instance_id",
} {
value := resource.Primary.Attributes[key]
t.Logf("%q = %q", key, value)
require.NotNil(t, value)
require.Greater(t, len(value), 0)
}
return nil
},
}},
})
}
110 changes: 110 additions & 0 deletions internal/provider/app.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package provider

import (
"context"
"net/url"

"github.com/google/uuid"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func appResource() *schema.Resource {
return &schema.Resource{
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())
return nil
},
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/icons. Use a " +
"built-in icon with `data.coder_workspace.me.access_url + \"/icons/<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
},
},
"name": {
Type: schema.TypeString,
Description: "A display name to identify the app.",
ForceNew: true,
Optional: true,
},
"relative_path": {
Type: schema.TypeBool,
Description: "Specifies whether the URL will be accessed via a relative " +
"path or wildcard. Use if wildcard routing is unavailable.",
ForceNew: true,
Optional: true,
ConflictsWith: []string{"command"},
},
"url": {
Type: schema.TypeString,
Description: "A URL to be proxied to from inside the workspace. " +
"Either \"command\" or \"url\" may be specified, but not both.",
ForceNew: true,
Optional: true,
ConflictsWith: []string{"command"},
},
"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,
},
},
},
},
},
}
}
Loading