Skip to content

Commit

Permalink
Add codec_server attribute to Namespace (#53)
Browse files Browse the repository at this point in the history
This commit allows namespaces to specify a `codec_server` object attribute that corresponds to the codec server in the API's NamespaceSpec.
  • Loading branch information
swgillespie committed Feb 14, 2024
1 parent 6b50edf commit 5923786
Show file tree
Hide file tree
Showing 5 changed files with 256 additions and 1 deletion.
14 changes: 14 additions & 0 deletions docs/resources/namespace.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ description: |-
### Optional

- `certificate_filters` (Attributes List) (see [below for nested schema](#nestedatt--certificate_filters))
- `codec_server` (Attributes) (see [below for nested schema](#nestedatt--codec_server))
- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts))

### Read-Only
Expand All @@ -42,6 +43,19 @@ Optional:
- `subject_alternative_name` (String)


<a id="nestedatt--codec_server"></a>
### Nested Schema for `codec_server`

Required:

- `endpoint` (String)

Optional:

- `include_cross_origin_credentials` (Boolean)
- `pass_access_token` (Boolean)


<a id="nestedblock--timeouts"></a>
### Nested Schema for `timeouts`

Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,5 @@ require (
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/genproto v0.0.0-20231106174013-bbf56f31fb17 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20231106174013-bbf56f31fb17 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)
4 changes: 3 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX
github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down Expand Up @@ -265,8 +266,9 @@ google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQ
google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
Expand Down
77 changes: 77 additions & 0 deletions internal/provider/namespace_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/hashicorp/terraform-plugin-framework/diag"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
Expand All @@ -58,6 +59,7 @@ type (
AcceptedClientCA types.String `tfsdk:"accepted_client_ca"`
RetentionDays types.Int64 `tfsdk:"retention_days"`
CertificateFilters types.List `tfsdk:"certificate_filters"`
CodecServer types.Object `tfsdk:"codec_server"`

Timeouts timeouts.Value `tfsdk:"timeouts"`
}
Expand All @@ -68,6 +70,12 @@ type (
OrganizationalUnit types.String `tfsdk:"organizational_unit"`
SubjectAlternativeName types.String `tfsdk:"subject_alternative_name"`
}

codecServerModel struct {
Endpoint types.String `tfsdk:"endpoint"`
PassAccessToken types.Bool `tfsdk:"pass_access_token"`
IncludeCrossOriginCredentials types.Bool `tfsdk:"include_cross_origin_credentials"`
}
)

var (
Expand All @@ -80,6 +88,12 @@ var (
"organizational_unit": types.StringType,
"subject_alternative_name": types.StringType,
}

codecServerAttrs = map[string]attr.Type{
"endpoint": types.StringType,
"pass_access_token": types.BoolType,
"include_cross_origin_credentials": types.BoolType,
}
)

func NewNamespaceResource() resource.Resource {
Expand Down Expand Up @@ -151,6 +165,24 @@ func (r *namespaceResource) Schema(ctx context.Context, _ resource.SchemaRequest
},
},
},
"codec_server": schema.SingleNestedAttribute{
Attributes: map[string]schema.Attribute{
"endpoint": schema.StringAttribute{
Required: true,
},
"pass_access_token": schema.BoolAttribute{
Computed: true,
Default: booldefault.StaticBool(false),
Optional: true,
},
"include_cross_origin_credentials": schema.BoolAttribute{
Computed: true,
Default: booldefault.StaticBool(false),
Optional: true,
},
},
Optional: true,
},
},
Blocks: map[string]schema.Block{
"timeouts": timeouts.Block(ctx, timeouts.Opts{
Expand Down Expand Up @@ -186,6 +218,13 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
if resp.Diagnostics.HasError() {
return
}
var codecServer *namespacev1.CodecServerSpec
if !plan.CodecServer.IsNull() {
codecServer = getCodecServerFromModel(ctx, resp.Diagnostics, &plan)
if resp.Diagnostics.HasError() {
return
}
}
svcResp, err := r.client.CreateNamespace(ctx, &cloudservicev1.CreateNamespaceRequest{
Spec: &namespacev1.NamespaceSpec{
Name: plan.Name.ValueString(),
Expand All @@ -195,6 +234,7 @@ func (r *namespaceResource) Create(ctx context.Context, req resource.CreateReque
AcceptedClientCa: plan.AcceptedClientCA.ValueString(),
CertificateFilters: certFilters,
},
CodecServer: codecServer,
},
})
if err != nil {
Expand Down Expand Up @@ -260,6 +300,10 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque
resp.Diagnostics.AddError("Failed to get current resource version", err.Error())
return
}
codecServer := getCodecServerFromModel(ctx, resp.Diagnostics, &plan)
if resp.Diagnostics.HasError() {
return
}
svcResp, err := r.client.UpdateNamespace(ctx, &cloudservicev1.UpdateNamespaceRequest{
Namespace: plan.ID.ValueString(),
Spec: &namespacev1.NamespaceSpec{
Expand All @@ -270,6 +314,7 @@ func (r *namespaceResource) Update(ctx context.Context, req resource.UpdateReque
AcceptedClientCa: plan.AcceptedClientCA.ValueString(),
CertificateFilters: certFilters,
},
CodecServer: codecServer,
},
ResourceVersion: resourceVersion,
})
Expand Down Expand Up @@ -380,6 +425,25 @@ func updateModelFromSpec(ctx context.Context, diags diag.Diagnostics, state *nam
certificateFilter = filters
}

var codecServerState basetypes.ObjectValue
// The API always returns a non-empty CodecServerSpec, even if it wasn't specified on object creation. We explicitly
// map an endpoint whose value is the empty string to `null`, since an empty endpoint implies that the codec server
// was not set via config.
if ns.GetSpec().GetCodecServer().GetEndpoint() != "" {
codecServer := &codecServerModel{
Endpoint: stringOrNull(ns.GetSpec().GetCodecServer().GetEndpoint()),
PassAccessToken: types.BoolValue(ns.GetSpec().GetCodecServer().GetPassAccessToken()),
IncludeCrossOriginCredentials: types.BoolValue(ns.GetSpec().GetCodecServer().GetIncludeCrossOriginCredentials()),
}

state, objectDiags := types.ObjectValueFrom(ctx, codecServerAttrs, codecServer)
diags.Append(objectDiags...)
codecServerState = state
} else {
codecServerState = types.ObjectNull(codecServerAttrs)
}

state.CodecServer = codecServerState
state.Regions = planRegions
state.CertificateFilters = certificateFilter
state.AcceptedClientCA = types.StringValue(ns.GetSpec().GetMtlsAuth().GetAcceptedClientCa())
Expand Down Expand Up @@ -427,6 +491,19 @@ func getCurrentResourceVersion(ctx context.Context, client cloudservicev1.CloudS
return ns.GetNamespace().GetResourceVersion(), nil
}

func getCodecServerFromModel(ctx context.Context, diags diag.Diagnostics, model *namespaceResourceModel) *namespacev1.CodecServerSpec {
var codecServer codecServerModel
diags.Append(model.CodecServer.As(ctx, &codecServer, basetypes.ObjectAsOptions{})...)
if diags.HasError() {
return nil
}
return &namespacev1.CodecServerSpec{
Endpoint: codecServer.Endpoint.ValueString(),
PassAccessToken: codecServer.PassAccessToken.ValueBool(),
IncludeCrossOriginCredentials: codecServer.IncludeCrossOriginCredentials.ValueBool(),
}
}

func stringOrNull(s string) types.String {
if s == "" {
return types.StringNull()
Expand Down
161 changes: 161 additions & 0 deletions internal/provider/namespace_resource_test.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package provider

import (
"bufio"
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"os"
"testing"
"text/template"
"time"

"github.com/hashicorp/terraform-plugin-testing/helper/resource"
"github.com/hashicorp/terraform-plugin-testing/terraform"

"github.com/temporalio/terraform-provider-temporalcloud/internal/client"
cloudservicev1 "github.com/temporalio/terraform-provider-temporalcloud/proto/go/temporal/api/cloud/cloudservice/v1"
)

func TestAccBasicNamespace(t *testing.T) {
Expand Down Expand Up @@ -114,6 +124,157 @@ PEM
})
}

func TestAccNamespaceWithCodecServer(t *testing.T) {
type (
codecServer struct {
Endpoint string
PassAccessToken bool
IncludeCrossOriginCredentials bool
}

configArgs struct {
Name string
RetentionDays int
CodecServer *codecServer
}
)

t.Parallel()
name := fmt.Sprintf("%s-%s", "tf-codec-server", randomString(10))
tmpl := template.Must(template.New("config").Parse(`
provider "temporalcloud" {
}
resource "temporalcloud_namespace" "test" {
name = "{{ .Name }}"
regions = ["aws-us-east-1"]
accepted_client_ca = base64encode(<<PEM
-----BEGIN CERTIFICATE-----
MIIByTCCAVCgAwIBAgIRAWHkC+6JUf3s9Tq43mdp2zgwCgYIKoZIzj0EAwMwEzER
MA8GA1UEChMIdGVtcG9yYWwwHhcNMjMwODEwMDAwOTQ1WhcNMjQwODA5MDAxMDQ1
WjATMREwDwYDVQQKEwh0ZW1wb3JhbDB2MBAGByqGSM49AgEGBSuBBAAiA2IABCzQ
7DwwGSQKM6Zrx3Qtw7IubfxiJ3RSXCqmcGhEbFVeocwAdEgMYlwSlUiWtDZVR2dM
XM9UZLWK4aGGnDNS5Mhcz6ibSBS7Owf4tRZZA9SpFCjNw2HraaiUVV+EUgxoe6No
MGYwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFG4N
8lIXqQKxwVs/ixVzdF6XGZm+MCQGA1UdEQQdMBuCGWNsaWVudC5yb290LnRlbXBv
cmFsLlB1VHMwCgYIKoZIzj0EAwMDZwAwZAIwRLfm9S7rKGd30KdQvUMcOcDJlmDw
6/oM6UOJFxLeGcpYbgxQ/bFize+Yx9Q9kNeMAjA7GiFsaipaKtWHy5MCOCas3ZP6
+ttLaXNXss3Z5Wk5vhDQnyE8JR3rPeQ2cHXLiA0=
-----END CERTIFICATE-----
PEM
)
retention_days = {{ .RetentionDays }}
{{ with .CodecServer }}
codec_server = {
endpoint = "{{ .Endpoint }}"
pass_access_token = {{ .PassAccessToken }}
include_cross_origin_credentials = {{ .IncludeCrossOriginCredentials }}
}
{{ end }}
}`))

config := func(args configArgs) string {
var buf bytes.Buffer
writer := bufio.NewWriter(&buf)
if err := tmpl.Execute(writer, args); err != nil {
t.Errorf("failed to execute template: %v", err)
t.FailNow()
}

writer.Flush()
return buf.String()
}

resource.Test(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
ProtoV6ProviderFactories: testAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: config(configArgs{
Name: name,
RetentionDays: 7,
}),
},
{
Config: config(configArgs{
Name: name,
RetentionDays: 7,
CodecServer: &codecServer{
Endpoint: "https://example.com",
PassAccessToken: true,
IncludeCrossOriginCredentials: true,
},
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t)
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}

spec := ns.Namespace.GetSpec()
if spec.GetCodecServer().GetEndpoint() != "https://example.com" {
return fmt.Errorf("unexpected endpoint: %s", spec.GetCodecServer().GetEndpoint())
}
if !spec.GetCodecServer().GetPassAccessToken() {
return errors.New("expected pass_access_token to be true")
}
if !spec.GetCodecServer().GetIncludeCrossOriginCredentials() {
return errors.New("expected include_cross_origin_credentials to be true")
}
return nil
},
},
{
Config: config(configArgs{
Name: name,
RetentionDays: 7,
}),
Check: func(s *terraform.State) error {
id := s.RootModule().Resources["temporalcloud_namespace.test"].Primary.Attributes["id"]
conn := newConnection(t)
ns, err := conn.GetNamespace(context.Background(), &cloudservicev1.GetNamespaceRequest{
Namespace: id,
})
if err != nil {
return fmt.Errorf("failed to get namespace: %v", err)
}

spec := ns.Namespace.GetSpec()
if spec.GetCodecServer().GetEndpoint() != "" {
return fmt.Errorf("unexpected endpoint: %s", spec.GetCodecServer().GetEndpoint())
}
return nil
},
},
// Delete testing automatically occurs in TestCase
},
})

}

func newConnection(t *testing.T) cloudservicev1.CloudServiceClient {
apiKey := os.Getenv("TEMPORAL_CLOUD_API_KEY")
endpoint := os.Getenv("TEMPORAL_CLOUD_ENDPOINT")
if endpoint == "" {
endpoint = "saas-api.tmprl.cloud:443"
}
allowInsecure := os.Getenv("TEMPORAL_CLOUD_ALLOW_INSECURE") == "true"

client, err := client.NewConnectionWithAPIKey(endpoint, allowInsecure, apiKey)
if err != nil {
t.Fatalf("Failed to create client: %v", err)
}

return client
}

func randomString(length int) string {
r := rand.New(rand.NewSource(time.Now().UTC().UnixNano()))
const charset = "abcdefghijklmnopqrstuvwxyz"
Expand Down

0 comments on commit 5923786

Please sign in to comment.