From d41c3698cf8dbd5c4aae0e815e6829ad822733dc Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Mon, 7 Mar 2022 20:37:49 +0000 Subject: [PATCH] feat: Use open-source Terraform Provider This removes our internal Terraform Provider, and opens it to the world! --- Makefile | 12 +- cmd/terraform-provider-coder/main.go | 13 -- go.mod | 2 +- provisioner/terraform/provider/provider.go | 182 ------------------ .../terraform/provider/provider_test.go | 152 --------------- provisioner/terraform/provision_test.go | 22 +-- provisionersdk/agent.go | 54 ++---- provisionersdk/agent_test.go | 21 +- 8 files changed, 25 insertions(+), 433 deletions(-) delete mode 100644 cmd/terraform-provider-coder/main.go delete mode 100644 provisioner/terraform/provider/provider.go delete mode 100644 provisioner/terraform/provider/provider_test.go diff --git a/Makefile b/Makefile index f3e1bfc8b0317..41412fb9e138f 100644 --- a/Makefile +++ b/Makefile @@ -12,12 +12,7 @@ bin/coderd: go build -o bin/coderd cmd/coderd/main.go .PHONY: bin/coderd -bin/terraform-provider-coder: - mkdir -p bin - go build -o bin/terraform-provider-coder cmd/terraform-provider-coder/main.go -.PHONY: bin/terraform-provider-coder - -build: site/out bin/coder bin/coderd bin/terraform-provider-coder +build: site/out bin/coder bin/coderd .PHONY: build # Runs migrations to output a dump of the database. @@ -66,11 +61,6 @@ install: @echo "-- CLI available at $(shell ls $(INSTALL_DIR)/coder*)" .PHONY: install -install/terraform-provider-coder: bin/terraform-provider-coder - $(eval OS_ARCH := $(shell go env GOOS)_$(shell go env GOARCH)) - mkdir -p ~/.terraform.d/plugins/coder.com/internal/coder/0.2/$(OS_ARCH) - cp bin/terraform-provider-coder ~/.terraform.d/plugins/coder.com/internal/coder/0.2/$(OS_ARCH) - peerbroker/proto: peerbroker/proto/peerbroker.proto protoc \ --go_out=. \ diff --git a/cmd/terraform-provider-coder/main.go b/cmd/terraform-provider-coder/main.go deleted file mode 100644 index 9102d33e461ef..0000000000000 --- a/cmd/terraform-provider-coder/main.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" - - "github.com/coder/coder/provisioner/terraform/provider" -) - -func main() { - plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: provider.New, - }) -} diff --git a/go.mod b/go.mod index d41fe46a35028..6bd8cc15c0a4a 100644 --- a/go.mod +++ b/go.mod @@ -57,7 +57,6 @@ require ( github.com/tabbed/pqtype v0.1.1 github.com/unrolled/secure v1.10.0 github.com/xlab/treeprint v1.1.0 - go.opencensus.io v0.23.0 go.uber.org/atomic v1.9.0 go.uber.org/goleak v1.1.12 golang.org/x/crypto v0.0.0-20220214200702-86341886e292 @@ -151,6 +150,7 @@ require ( github.com/xeipuuv/gojsonschema v1.2.0 // indirect github.com/zclconf/go-cty v1.10.0 // indirect github.com/zeebo/errs v1.2.2 // indirect + go.opencensus.io v0.23.0 // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/text v0.3.7 // indirect diff --git a/provisioner/terraform/provider/provider.go b/provisioner/terraform/provider/provider.go deleted file mode 100644 index ca3148bade0f2..0000000000000 --- a/provisioner/terraform/provider/provider.go +++ /dev/null @@ -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, - }, - }, - }, - }, - } -} diff --git a/provisioner/terraform/provider/provider_test.go b/provisioner/terraform/provider/provider_test.go deleted file mode 100644 index c77db5de85f9a..0000000000000 --- a/provisioner/terraform/provider/provider_test.go +++ /dev/null @@ -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 - }, - }}, - }) - }) -} diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index 507828e9f04d2..6916d67da6352 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -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" } } } diff --git a/provisionersdk/agent.go b/provisionersdk/agent.go index 2f2a3a74dc68e..6faa2db8947db 100644 --- a/provisionersdk/agent.go +++ b/provisionersdk/agent.go @@ -1,23 +1,17 @@ package provisionersdk -import ( - "fmt" - "net/url" - "strings" - - "golang.org/x/xerrors" -) +import "fmt" var ( // A mapping of operating-system ($GOOS) to architecture ($GOARCH) // to agent install and run script. ${DOWNLOAD_URL} is replaced // with strings.ReplaceAll() when being consumed. - agentScript = map[string]map[string]string{ + agentScripts = map[string]map[string]string{ "windows": { "amd64": ` $ProgressPreference = "SilentlyContinue" $ErrorActionPreference = "Stop" -Invoke-WebRequest -Uri ${DOWNLOAD_URL} -OutFile $env:TEMP\coder.exe +Invoke-WebRequest -Uri ${ACCESS_URL}/bin/coder-windows-amd64 -OutFile $env:TEMP\coder.exe $env:CODER_URL = "${ACCESS_URL}" Start-Process -FilePath $env:TEMP\coder.exe workspaces agent `, @@ -27,7 +21,7 @@ Start-Process -FilePath $env:TEMP\coder.exe workspaces agent #!/usr/bin/env sh set -eu pipefail BINARY_LOCATION=$(mktemp -d)/coder -curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION +curl -fsSL ${ACCESS_URL}/bin/coder-linux-amd64 -o $BINARY_LOCATION chmod +x $BINARY_LOCATION export CODER_URL="${ACCESS_URL}" exec $BINARY_LOCATION agent @@ -38,7 +32,7 @@ exec $BINARY_LOCATION agent #!/usr/bin/env sh set -eu pipefail BINARY_LOCATION=$(mktemp -d)/coder -curl -fsSL ${DOWNLOAD_URL} -o $BINARY_LOCATION +curl -fsSL ${ACCESS_URL}/bin/coder-darwin-amd64 -o $BINARY_LOCATION chmod +x $BINARY_LOCATION export CODER_URL="${ACCESS_URL}" exec $BINARY_LOCATION agent @@ -47,35 +41,15 @@ exec $BINARY_LOCATION agent } ) -// AgentScript returns an installation script for the specified operating system -// and architecture. -func AgentScript(coderURL *url.URL, operatingSystem, architecture string) (string, error) { - architectures, exists := agentScript[operatingSystem] - if !exists { - list := []string{} - for key := range agentScript { - list = append(list, key) - } - return "", xerrors.Errorf("operating system %q not supported. must be in: %v", operatingSystem, list) - } - script, exists := architectures[architecture] - if !exists { - list := []string{} - for key := range architectures { - list = append(list, key) +// AgentScriptEnv returns a key-pair of scripts that are consumed +// by the Coder Terraform Provider. See: +// https://github.com/coder/terraform-provider-coder/blob/main/internal/provider/provider.go#L97 +func AgentScriptEnv() map[string]string { + env := map[string]string{} + for operatingSystem, scripts := range agentScripts { + for architecture, script := range scripts { + env[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", operatingSystem, architecture)] = script } - return "", xerrors.Errorf("architecture %q not supported for %q. must be in: %v", architecture, operatingSystem, list) - } - downloadURL, err := coderURL.Parse(fmt.Sprintf("/bin/coder-%s-%s", operatingSystem, architecture)) - if err != nil { - return "", xerrors.Errorf("parse download url: %w", err) - } - accessURL, err := coderURL.Parse("/") - if err != nil { - return "", xerrors.Errorf("parse access url: %w", err) } - return strings.NewReplacer( - "${DOWNLOAD_URL}", downloadURL.String(), - "${ACCESS_URL}", accessURL.String(), - ).Replace(script), nil + return env } diff --git a/provisionersdk/agent_test.go b/provisionersdk/agent_test.go index b5ba3924f619f..767ba966bae55 100644 --- a/provisionersdk/agent_test.go +++ b/provisionersdk/agent_test.go @@ -7,6 +7,7 @@ package provisionersdk_test import ( + "fmt" "net/http" "net/http/httptest" "net/url" @@ -37,9 +38,13 @@ func TestAgentScript(t *testing.T) { t.Cleanup(srv.Close) srvURL, err := url.Parse(srv.URL) require.NoError(t, err) - script, err := provisionersdk.AgentScript(srvURL, runtime.GOOS, runtime.GOARCH) - require.NoError(t, err) + script, exists := provisionersdk.AgentScriptEnv()[fmt.Sprintf("CODER_AGENT_SCRIPT_%s_%s", runtime.GOOS, runtime.GOARCH)] + if !exists { + t.Skip("Agent not supported...") + return + } + script = strings.ReplaceAll(script, "${ACCESS_URL}", srvURL.String()) output, err := exec.Command("sh", "-c", script).CombinedOutput() t.Log(string(output)) require.NoError(t, err) @@ -47,16 +52,4 @@ func TestAgentScript(t *testing.T) { // as the response to executing our script. require.Equal(t, "agent", strings.TrimSpace(string(output))) }) - - t.Run("UnsupportedOS", func(t *testing.T) { - t.Parallel() - _, err := provisionersdk.AgentScript(nil, "unsupported", "") - require.Error(t, err) - }) - - t.Run("UnsupportedArch", func(t *testing.T) { - t.Parallel() - _, err := provisionersdk.AgentScript(nil, runtime.GOOS, "unsupported") - require.Error(t, err) - }) }