diff --git a/Makefile b/Makefile index 38a9a69..7a771e4 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ build: terraform-provider-coder # Builds the provider. Note that as coder/coder is based on # alpine, we need to disable cgo. -terraform-provider-coder: provider/*.go main.go +terraform-provider-coder: provider/*.go tpfprovider/*.go main.go CGO_ENABLED=0 go build . # Run integration tests @@ -22,4 +22,4 @@ test-integration: terraform-provider-coder # Run acceptance tests .PHONY: testacc testacc: - TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m + TF_ACC=1 go test ./... -v -count=1 $(TESTARGS) -timeout 120m diff --git a/go.mod b/go.mod index c1033b5..2119e27 100644 --- a/go.mod +++ b/go.mod @@ -49,8 +49,11 @@ require ( github.com/hashicorp/logutils v1.0.0 // indirect github.com/hashicorp/terraform-exec v0.22.0 // indirect github.com/hashicorp/terraform-json v0.24.0 // indirect + github.com/hashicorp/terraform-plugin-framework v1.14.1 // indirect github.com/hashicorp/terraform-plugin-go v0.26.0 // indirect github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect + github.com/hashicorp/terraform-plugin-mux v0.18.0 // indirect + github.com/hashicorp/terraform-plugin-testing v1.11.0 // indirect github.com/hashicorp/terraform-registry-address v0.2.4 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect github.com/hashicorp/yamux v0.1.1 // indirect diff --git a/go.sum b/go.sum index a881909..cd6e7d0 100644 --- a/go.sum +++ b/go.sum @@ -105,12 +105,18 @@ github.com/hashicorp/terraform-exec v0.22.0 h1:G5+4Sz6jYZfRYUCg6eQgDsqTzkNXV+fP8 github.com/hashicorp/terraform-exec v0.22.0/go.mod h1:bjVbsncaeh8jVdhttWYZuBGj21FcYw6Ia/XfHcNO7lQ= github.com/hashicorp/terraform-json v0.24.0 h1:rUiyF+x1kYawXeRth6fKFm/MdfBS6+lW4NbeATsYz8Q= github.com/hashicorp/terraform-json v0.24.0/go.mod h1:Nfj5ubo9xbu9uiAoZVBsNOjvNKB66Oyrvtit74kC7ow= +github.com/hashicorp/terraform-plugin-framework v1.14.1 h1:jaT1yvU/kEKEsxnbrn4ZHlgcxyIfjvZ41BLdlLk52fY= +github.com/hashicorp/terraform-plugin-framework v1.14.1/go.mod h1:xNUKmvTs6ldbwTuId5euAtg37dTxuyj3LHS3uj7BHQ4= github.com/hashicorp/terraform-plugin-go v0.26.0 h1:cuIzCv4qwigug3OS7iKhpGAbZTiypAfFQmw8aE65O2M= github.com/hashicorp/terraform-plugin-go v0.26.0/go.mod h1:+CXjuLDiFgqR+GcrM5a2E2Kal5t5q2jb0E3D57tTdNY= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-mux v0.18.0 h1:7491JFSpWyAe0v9YqBT+kel7mzHAbO5EpxxT0cUL/Ms= +github.com/hashicorp/terraform-plugin-mux v0.18.0/go.mod h1:Ho1g4Rr8qv0qTJlcRKfjjXTIO67LNbDtM6r+zHUNHJQ= github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0 h1:7/iejAPyCRBhqAg3jOx+4UcAhY0A+Sg8B+0+d/GxSfM= github.com/hashicorp/terraform-plugin-sdk/v2 v2.36.0/go.mod h1:TiQwXAjFrgBf5tg5rvBRz8/ubPULpU0HjSaVi5UoJf8= +github.com/hashicorp/terraform-plugin-testing v1.11.0 h1:MeDT5W3YHbONJt2aPQyaBsgQeAIckwPX41EUHXEn29A= +github.com/hashicorp/terraform-plugin-testing v1.11.0/go.mod h1:WNAHQ3DcgV/0J+B15WTE6hDvxcUdkPPpnB1FR3M910U= github.com/hashicorp/terraform-registry-address v0.2.4 h1:JXu/zHB2Ymg/TGVCRu10XqNa4Sh2bWcqCNyKWjnCPJA= github.com/hashicorp/terraform-registry-address v0.2.4/go.mod h1:tUNYTVyCtU4OIGXXMDp7WNcJ+0W1B4nmstVDgHMjfAU= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= diff --git a/main.go b/main.go index 2eaa5dc..3a732fe 100644 --- a/main.go +++ b/main.go @@ -1,9 +1,19 @@ package main import ( - "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" + "context" + "flag" + "log" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-go/tfprotov6/tf6server" + "github.com/hashicorp/terraform-plugin-mux/tf5to6server" + "github.com/hashicorp/terraform-plugin-mux/tf6muxserver" "github.com/coder/terraform-provider-coder/v2/provider" + "github.com/coder/terraform-provider-coder/v2/tpfprovider" ) // Run the docs generation tool, check its repository for more information on how it works and how docs @@ -11,8 +21,45 @@ import ( //go:generate go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs func main() { + ctx := context.Background() + var debug bool + flag.BoolVar(&debug, "debug", false, "enable debug logging") + flag.Parse() + servePprof() - plugin.Serve(&plugin.ServeOpts{ - ProviderFunc: provider.New, - }) + + upgradedSDKServer, err := tf5to6server.UpgradeServer( + ctx, + provider.New().GRPCProvider, + ) + if err != nil { + log.Fatal(err) + } + + providers := []func() tfprotov6.ProviderServer{ + providerserver.NewProtocol6(tpfprovider.NewFrameworkProvider()()), + func() tfprotov6.ProviderServer { + return upgradedSDKServer + }, + } + + muxServer, err := tf6muxserver.NewMuxServer(ctx, providers...) + if err != nil { + log.Fatal(err) + } + + var serveOpts []tf6server.ServeOpt + if debug { + serveOpts = append(serveOpts, tf6server.WithManagedDebug()) + } + + err = tf6server.Serve( + "registry.terraform.io/coder/coder", + muxServer.ProviderServer, + serveOpts..., + ) + + if err != nil { + log.Fatal(err) + } } diff --git a/tpfprovider/parameter.go b/tpfprovider/parameter.go new file mode 100644 index 0000000..94a6556 --- /dev/null +++ b/tpfprovider/parameter.go @@ -0,0 +1,100 @@ +package tpfprovider + +import ( + "context" + "encoding/json" + "math/big" + "os" + "strings" + + "github.com/coder/terraform-provider-coder/v2/provider" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +type parameterDataSourceModel struct { + Name types.String `tfsdk:"name"` + Type types.Dynamic `tfsdk:"type"` + Value types.Dynamic `tfsdk:"value"` +} + +type parameterDataSource struct{} + +func NewParameterDataSource() datasource.DataSource { + return ¶meterDataSource{} +} + +func (m *parameterDataSource) Metadata(ctx context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = "coder_parameter" +} + +func (m *parameterDataSource) Schema(ctx context.Context, req datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + Required: true, + }, + "type": schema.DynamicAttribute{ + Required: true, + }, + "value": schema.DynamicAttribute{ + Computed: true, + }, + }, + } +} + +func (m *parameterDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data parameterDataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + if data.Name.IsNull() { + resp.Diagnostics.AddError("name is required", "name") + return + } + + ds := data.Name.ValueString() + parameterEnv := provider.ParameterEnvironmentVariable(ds) + rawValue, ok := os.LookupEnv(parameterEnv) + if !ok { + resp.Diagnostics.AddError("parameter not found", "name") + return + } + + switch data.Type.UnderlyingValue().(type) { + case types.String: + data.Value = types.DynamicValue(types.StringValue(rawValue)) + case types.Number: + // convert the raw value to a number + var floatVal float64 + if err := json.NewDecoder(strings.NewReader(rawValue)).Decode(&floatVal); err != nil { + resp.Diagnostics.AddError("failed to parse value as number", "value") + return + } + + data.Value = types.DynamicValue(types.NumberValue(big.NewFloat(floatVal))) + case types.Bool: + // convert the raw value to a bool + var boolVal bool + if err := json.NewDecoder(strings.NewReader(rawValue)).Decode(&boolVal); err != nil { + resp.Diagnostics.AddError("failed to parse value as bool", "value") + return + } + data.Value = types.DynamicValue(types.BoolValue(boolVal)) + case types.List: + // TODO: handle list + resp.Diagnostics.AddError("TODO: list type not supported", "type") + return + case types.Map: + // TODO: handle map + resp.Diagnostics.AddError("TODO: map type not supported", "type") + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} diff --git a/tpfprovider/parameter_test.go b/tpfprovider/parameter_test.go new file mode 100644 index 0000000..8138f60 --- /dev/null +++ b/tpfprovider/parameter_test.go @@ -0,0 +1,151 @@ +package tpfprovider + +import ( + "fmt" + "os" + "testing" + + "github.com/coder/terraform-provider-coder/v2/provider" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" +) + +func TestAccParameterDataSource(t *testing.T) { + t.Run("string", func(t *testing.T) { + t.Setenv(provider.ParameterEnvironmentVariable("test"), "test") + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{{ + Config: ` +provider coder {} +data "coder_parameter" "test" { + name = "test" + type = "" +}`, + Check: resource.ComposeAggregateTestCheckFunc( + testParameterEnv("test", "test"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "name", "test"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "type", ""), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value", "test"), + ), + }}, + }) + }) + + t.Run("number", func(t *testing.T) { + t.Setenv(provider.ParameterEnvironmentVariable("test"), "3.14") + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{{ + Config: ` +provider coder {} +data "coder_parameter" "test" { + name = "test" + type = 0 +}`, + Check: resource.ComposeAggregateTestCheckFunc( + testParameterEnv("test", "3.14"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "name", "test"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "type", "0"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value", "3.14"), + ), + }}, + }) + }) + + t.Run("bool", func(t *testing.T) { + t.Setenv(provider.ParameterEnvironmentVariable("test"), "true") + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{{ + Config: ` +provider coder {} +data "coder_parameter" "test" { + name = "test" + type = false +}`, + Check: resource.ComposeAggregateTestCheckFunc( + testParameterEnv("test", "true"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "name", "test"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "type", "false"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value", "true"), + ), + }}, + }) + }) + + t.Run("list of string", func(t *testing.T) { + t.Skip("TODO: not implemented yet") + t.Setenv(provider.ParameterEnvironmentVariable("test"), `["a","b","c"]`) + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{{ + Config: ` +provider coder {} +data "coder_parameter" "test" { + name = "test" + type = [""] +}`, + Check: resource.ComposeAggregateTestCheckFunc( + testParameterEnv("test", `["a","b","c"]`), + resource.TestCheckResourceAttr("data.coder_parameter.test", "name", "test"), + resource.TestCheckTypeSetElemAttr("data.coder_parameter.test", "type*", "[]"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.#", "3"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.0", "a"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.1", "b"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.2", "c"), + ), + }}, + }) + }) + + t.Run("list of number", func(t *testing.T) { + t.Skip("TODO: not implemented yet") + t.Setenv(provider.ParameterEnvironmentVariable("test"), `[1, 2, 3]`) + resource.Test(t, resource.TestCase{ + IsUnitTest: true, + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{{ + Config: ` +provider coder {} +data "coder_parameter" "test" { + name = "test" + type = [0] +} +`, + Check: resource.ComposeAggregateTestCheckFunc( + testParameterEnv("test", `[1,2,3]`), + resource.TestCheckResourceAttr("data.coder_parameter.test", "name", "test"), + resource.TestCheckTypeSetElemAttr("data.coder_parameter.test", "type*", "[]"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.#", "3"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.0", "1"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.1", "2"), + resource.TestCheckResourceAttr("data.coder_parameter.test", "value.2", "3"), + ), + }}, + }) + }) +} + +func testParameterEnv(name, value string) func(*terraform.State) error { + return func(*terraform.State) error { + penv := provider.ParameterEnvironmentVariable("test") + val, ok := os.LookupEnv(penv) + if !ok { + return fmt.Errorf("parameter environment variable %q not set", penv) + } + if val != value { + return fmt.Errorf("parameter environment variable %q has unexpected value %q", penv, val) + } + return nil + } +} diff --git a/tpfprovider/tpfprovider.go b/tpfprovider/tpfprovider.go new file mode 100644 index 0000000..cb501c2 --- /dev/null +++ b/tpfprovider/tpfprovider.go @@ -0,0 +1,52 @@ +package tpfprovider + +import ( + "context" + "net/url" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/provider" + "github.com/hashicorp/terraform-plugin-framework/provider/schema" + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +type coderProvider struct { + URL *url.URL +} + +var _ provider.Provider = (*coderProvider)(nil) + +func NewFrameworkProvider() func() provider.Provider { + return func() provider.Provider { + return &coderProvider{} + } +} + +func (p *coderProvider) Resources(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{} +} + +func (p *coderProvider) DataSources(_ context.Context) []func() datasource.DataSource { + return []func() datasource.DataSource{ + NewParameterDataSource, + } +} + +func (p *coderProvider) Schema(_ context.Context, _ provider.SchemaRequest, resp *provider.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "url": schema.StringAttribute{ + Description: "The URL to access Coder.", + Optional: true, + }, + }, + } +} + +func (p *coderProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) { + resp.TypeName = "coder" +} + +func (p *coderProvider) Configure(_ context.Context, _ provider.ConfigureRequest, resp *provider.ConfigureResponse) { + +} diff --git a/tpfprovider/tpfprovider_test.go b/tpfprovider/tpfprovider_test.go new file mode 100644 index 0000000..d1ae482 --- /dev/null +++ b/tpfprovider/tpfprovider_test.go @@ -0,0 +1,16 @@ +package tpfprovider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" +) + +var testAccProtoV6ProviderFactories = map[string]func() (tfprotov6.ProviderServer, error){ + "coder": providerserver.NewProtocol6WithError(NewFrameworkProvider()()), +} + +func testAccPreCheck(t *testing.T) { + t.Helper() +}