feat: Use open-source Terraform Provider (#403)

This removes our internal Terraform Provider, and opens
it to the world!
This commit is contained in:
Kyle Carberry
2022-03-07 17:39:00 -06:00
committed by GitHub
parent bf0ae8f573
commit 18c929c8ab
8 changed files with 25 additions and 433 deletions
-182
View File
@@ -1,182 +0,0 @@
package provider
import (
"context"
"net/url"
"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"
"github.com/coder/coder/database"
"github.com/coder/coder/provisionersdk"
)
type config struct {
URL *url.URL
}
// New returns a new Terraform provider.
func New() *schema.Provider {
return &schema.Provider{
Schema: map[string]*schema.Schema{
"url": {
Type: schema.TypeString,
Optional: true,
// The "CODER_URL" environment variable is used by default
// as the Access URL when generating scripts.
DefaultFunc: schema.EnvDefaultFunc("CODER_URL", ""),
ValidateFunc: func(i interface{}, s string) ([]string, []error) {
_, err := url.Parse(s)
if err != nil {
return nil, []error{err}
}
return nil, nil
},
},
},
ConfigureContextFunc: func(c context.Context, resourceData *schema.ResourceData) (interface{}, diag.Diagnostics) {
rawURL, ok := resourceData.Get("url").(string)
if !ok {
return nil, diag.Errorf("unexpected type %q for url", reflect.TypeOf(resourceData.Get("url")).String())
}
if rawURL == "" {
return nil, diag.Errorf("CODER_URL must not be empty; got %q", rawURL)
}
parsed, err := url.Parse(resourceData.Get("url").(string))
if err != nil {
return nil, diag.FromErr(err)
}
return config{
URL: parsed,
}, nil
},
DataSourcesMap: map[string]*schema.Resource{
"coder_workspace": {
Description: "TODO",
ReadContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
rd.SetId(uuid.NewString())
return nil
},
Schema: map[string]*schema.Schema{
"transition": {
Type: schema.TypeString,
Optional: true,
Description: "TODO",
DefaultFunc: schema.EnvDefaultFunc("CODER_WORKSPACE_TRANSITION", ""),
ValidateFunc: validation.StringInSlice([]string{string(database.WorkspaceTransitionStart), string(database.WorkspaceTransitionStop)}, false),
},
},
},
"coder_agent_script": {
Description: "TODO",
ReadContext: func(c context.Context, 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())
}
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")))
}
script, err := provisionersdk.AgentScript(config.URL, operatingSystem, arch)
if err != nil {
return diag.FromErr(err)
}
err = resourceData.Set("value", script)
if err != nil {
return diag.FromErr(err)
}
resourceData.SetId(strings.Join([]string{operatingSystem, arch}, "_"))
return nil
},
Schema: map[string]*schema.Schema{
"os": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"linux", "darwin", "windows"}, false),
},
"arch": {
Type: schema.TypeString,
Required: true,
ValidateFunc: validation.StringInSlice([]string{"amd64"}, false),
},
"value": {
Type: schema.TypeString,
Computed: true,
},
},
},
},
ResourcesMap: map[string]*schema.Resource{
"coder_agent": {
Description: "TODO",
CreateContext: func(c context.Context, rd *schema.ResourceData, i interface{}) diag.Diagnostics {
// This should be a real authentication token!
rd.SetId(uuid.NewString())
err := rd.Set("token", uuid.NewString())
if err != nil {
return diag.FromErr(err)
}
return nil
},
ReadContext: func(c context.Context, rd *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{
"auth": {
ForceNew: true,
Description: "TODO",
Type: schema.TypeList,
Optional: true,
MaxItems: 1,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"type": {
ForceNew: true,
Description: "TODO",
Optional: true,
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{"google-instance-identity"}, false),
},
"instance_id": {
ForceNew: true,
Description: "TODO",
Optional: true,
Type: schema.TypeString,
},
},
},
},
"env": {
ForceNew: true,
Description: "TODO",
Type: schema.TypeMap,
Optional: true,
},
"startup_script": {
ForceNew: true,
Description: "TODO",
Type: schema.TypeString,
Optional: true,
},
"token": {
ForceNew: true,
Type: schema.TypeString,
Computed: true,
},
},
},
},
}
}
@@ -1,152 +0,0 @@
package provider_test
import (
"fmt"
"testing"
"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"
"github.com/coder/coder/provisioner/terraform/provider"
)
func TestProvider(t *testing.T) {
t.Parallel()
tfProvider := provider.New()
err := tfProvider.InternalValidate()
require.NoError(t, err)
}
func TestWorkspace(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"
}
data "coder_workspace" "me" {
transition = "start"
}`,
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["data.coder_workspace.me"]
require.NotNil(t, resource)
value := resource.Primary.Attributes["transition"]
require.NotNil(t, value)
t.Log(value)
return nil
},
}},
})
}
func TestAgentScript(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"
}
data "coder_agent_script" "new" {
arch = "amd64"
os = "linux"
}`,
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["data.coder_agent_script.new"]
require.NotNil(t, resource)
value := resource.Primary.Attributes["value"]
require.NotNil(t, value)
t.Log(value)
return nil
},
}},
})
}
func TestAgent(t *testing.T) {
t.Parallel()
t.Run("Empty", func(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" {}`,
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)
require.NotNil(t, resource.Primary.Attributes["token"])
return nil
},
}},
})
})
t.Run("Filled", func(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" {
auth {
type = "google-instance-identity"
instance_id = "instance"
}
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",
"auth.0.type",
"auth.0.instance_id",
"env.hi",
"startup_script",
} {
value := resource.Primary.Attributes[key]
t.Log(fmt.Sprintf("%q = %q", key, value))
require.NotNil(t, value)
require.Greater(t, len(value), 0)
}
return nil
},
}},
})
})
}
+2 -20
View File
@@ -5,11 +5,8 @@ package terraform_test
import (
"context"
"encoding/json"
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/stretchr/testify/require"
@@ -25,27 +22,12 @@ import (
func TestProvision(t *testing.T) {
t.Parallel()
// Build and output the Terraform Provider that is consumed for these tests.
homeDir, err := os.UserHomeDir()
require.NoError(t, err)
providerDest := filepath.Join(homeDir, ".terraform.d", "plugins", "coder.com", "internal", "coder", "0.0.1", fmt.Sprintf("%s_%s", runtime.GOOS, runtime.GOARCH))
err = os.MkdirAll(providerDest, 0700)
require.NoError(t, err)
//nolint:dogsled
_, filename, _, _ := runtime.Caller(0)
providerSrc := filepath.Join(filepath.Dir(filename), "..", "..", "cmd", "terraform-provider-coder")
output, err := exec.Command("go", "build", "-o", providerDest, providerSrc).CombinedOutput()
if err != nil {
t.Log(string(output))
}
require.NoError(t, err)
provider := `
terraform {
required_providers {
coder = {
source = "coder.com/internal/coder"
version = "0.0.1"
source = "coder/coder"
version = "0.1.0"
}
}
}