diff --git a/example/main.tf b/example/main.tf index c05247b..913eb44 100644 --- a/example/main.tf +++ b/example/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { pomerium = { source = "pomerium/pomerium" - version = "0.0.7" + version = "0.0.8" } } } @@ -249,15 +249,5 @@ data "pomerium_route" "existing_route" { id = pomerium_route.test_route.id } -# Output examples -output "namespace_name" { - value = data.pomerium_namespace.existing_namespace.name -} - -# output "route_from" { -# value = data.pomerium_route.existing_route.from -# } +data "pomerium_routes" "all_routes" {} -output "all_namespaces" { - value = data.pomerium_namespaces.all_namespaces.namespaces -} diff --git a/internal/provider/enum.go b/internal/provider/enum.go new file mode 100644 index 0000000..f2ba6d7 --- /dev/null +++ b/internal/provider/enum.go @@ -0,0 +1,62 @@ +package provider + +import ( + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// GetValidEnumValues returns a list of valid enum values for a given protobuf enum type. +// it includes zero value as well to match its use in the current api +func GetValidEnumValues[T protoreflect.Enum]() []string { + var values []string + var v T + descriptor := v.Descriptor() + for i := 0; i < descriptor.Values().Len(); i++ { + values = append(values, string(descriptor.Values().Get(i).Name())) + } + return values +} + +// EnumValueToPBWithDefault converts a string to a protobuf enum value. +func EnumValueToPBWithDefault[T interface { + ~int32 + protoreflect.Enum +}]( + dst *T, + src types.String, + defaultValue T, + diagnostics *diag.Diagnostics, +) { + if src.IsNull() || src.ValueString() == "" { + *dst = defaultValue + return + } + + var v T + enumValue := v.Descriptor().Values().ByName(protoreflect.Name(src.ValueString())) + if enumValue == nil { + diagnostics.AddError( + "InvalidEnumValue", + fmt.Sprintf("The provided %s enum value %q is not valid.", v.Descriptor().FullName(), src.ValueString()), + ) + return + } + + *dst = T(enumValue.Number()) +} + +func EnumValueFromPB[T interface { + ~int32 + protoreflect.Enum +}]( + src T, +) types.String { + v := src.Descriptor().Values().ByNumber(protoreflect.EnumNumber(src)) + if v == nil { + return types.StringNull() + } + return types.StringValue(string(v.Name())) +} diff --git a/internal/provider/enum_test.go b/internal/provider/enum_test.go new file mode 100644 index 0000000..1f3940e --- /dev/null +++ b/internal/provider/enum_test.go @@ -0,0 +1,63 @@ +package provider_test + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pomerium/enterprise-client-go/pb" + "github.com/pomerium/enterprise-terraform-provider/internal/provider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestEnumValueToPB(t *testing.T) { + t.Parallel() + + defaultValue := pb.IssuerFormat(-1) + tests := []struct { + name types.String + expect pb.IssuerFormat + expectError bool + }{ + {types.StringValue("IssuerHostOnly"), pb.IssuerFormat_IssuerHostOnly, false}, + {types.StringValue("IssuerURI"), pb.IssuerFormat_IssuerURI, false}, + {types.StringValue("InvalidInexistentTest"), pb.IssuerFormat(-2), true}, + {types.StringNull(), defaultValue, false}, + {types.StringValue(""), defaultValue, false}, + } + + for _, tt := range tests { + t.Run(tt.name.String(), func(t *testing.T) { + var got pb.IssuerFormat + var diagnostics diag.Diagnostics + provider.EnumValueToPBWithDefault(&got, tt.name, defaultValue, &diagnostics) + if tt.expectError { + assert.True(t, diagnostics.HasError()) + } else { + require.False(t, diagnostics.HasError(), diagnostics.Errors()) + assert.Equal(t, tt.expect, got) + } + }) + } +} + +func TestEnumValueFromPB(t *testing.T) { + t.Parallel() + + tests := []struct { + name pb.IssuerFormat + expect types.String + }{ + {pb.IssuerFormat_IssuerHostOnly, types.StringValue("IssuerHostOnly")}, + {pb.IssuerFormat_IssuerURI, types.StringValue("IssuerURI")}, + {pb.IssuerFormat(-1), types.StringNull()}, + } + + for _, tt := range tests { + t.Run(tt.expect.String(), func(t *testing.T) { + got := provider.EnumValueFromPB(tt.name) + assert.Equal(t, tt.expect, got) + }) + } +} diff --git a/internal/provider/route.go b/internal/provider/route.go index 9bb5e5c..59dd550 100644 --- a/internal/provider/route.go +++ b/internal/provider/route.go @@ -5,12 +5,13 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" - "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-log/tflog" client "github.com/pomerium/enterprise-client-go" @@ -200,11 +201,12 @@ func (r *RouteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp Computed: true, }, "jwt_groups_filter": JWTGroupsFilterSchema, - "jwt_issuer_format": schema.ObjectAttribute{ - Description: "JWT issuer format configuration.", + "jwt_issuer_format": schema.StringAttribute{ Optional: true, - AttributeTypes: map[string]attr.Type{ - "format": types.StringType, + Computed: true, + Description: "Format for JWT issuer strings. Use 'IssuerHostOnly' for hostname without scheme or trailing slash, or 'IssuerURI' for complete URI including scheme and trailing slash.", + Validators: []validator.String{ + stringvalidator.OneOf(GetValidEnumValues[pb.IssuerFormat]()...), }, }, "rewrite_response_headers": schema.SetNestedAttribute{ diff --git a/internal/provider/route_data_source.go b/internal/provider/route_data_source.go index fb7d82b..53df64d 100644 --- a/internal/provider/route_data_source.go +++ b/internal/provider/route_data_source.go @@ -5,9 +5,10 @@ import ( "fmt" "github.com/hashicorp/terraform-plugin-framework-timetypes/timetypes" - "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/datasource" "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" "github.com/hashicorp/terraform-plugin-framework/types" client "github.com/pomerium/enterprise-client-go" @@ -167,11 +168,12 @@ func getRouteDataSourceAttributes(idRequired bool) map[string]schema.Attribute { Description: "Show error details.", }, "jwt_groups_filter": JWTGroupsFilterSchema, - "jwt_issuer_format": schema.ObjectAttribute{ - Description: "JWT issuer format configuration.", + "jwt_issuer_format": schema.StringAttribute{ + Optional: true, Computed: true, - AttributeTypes: map[string]attr.Type{ - "format": types.StringType, + Description: "Format for JWT issuer strings. Use 'IssuerHostOnly' for hostname without scheme or trailing slash, or 'IssuerURI' for complete URI including scheme and trailing slash.", + Validators: []validator.String{ + stringvalidator.OneOf(GetValidEnumValues[pb.IssuerFormat]()...), }, }, "rewrite_response_headers": schema.SetNestedAttribute{ diff --git a/internal/provider/route_model.go b/internal/provider/route_model.go index d43f1cf..d400c8a 100644 --- a/internal/provider/route_model.go +++ b/internal/provider/route_model.go @@ -26,7 +26,7 @@ type RouteModel struct { IDPClientID types.String `tfsdk:"idp_client_id"` IDPClientSecret types.String `tfsdk:"idp_client_secret"` JWTGroupsFilter types.Object `tfsdk:"jwt_groups_filter"` - JWTIssuerFormat types.Object `tfsdk:"jwt_issuer_format"` + JWTIssuerFormat types.String `tfsdk:"jwt_issuer_format"` KubernetesServiceAccountToken types.String `tfsdk:"kubernetes_service_account_token"` KubernetesServiceAccountTokenFile types.String `tfsdk:"kubernetes_service_account_token_file"` LogoURL types.String `tfsdk:"logo_url"` @@ -174,6 +174,7 @@ func ConvertRouteToPB( pbRoute.EnableGoogleCloudServerlessAuthentication = src.EnableGoogleCloudServerlessAuthentication.ValueBool() } pbRoute.KubernetesServiceAccountTokenFile = src.KubernetesServiceAccountTokenFile.ValueStringPointer() + EnumValueToPBWithDefault(&pbRoute.JwtIssuerFormat, src.JWTIssuerFormat, pb.IssuerFormat_IssuerHostOnly, &diagnostics) pbRoute.RewriteResponseHeaders = rewriteHeadersToPB(src.RewriteResponseHeaders) @@ -232,6 +233,7 @@ func ConvertRouteFromPB( } dst.KubernetesServiceAccountTokenFile = types.StringPointerValue(src.KubernetesServiceAccountTokenFile) + dst.JWTIssuerFormat = EnumValueFromPB(src.JwtIssuerFormat) dst.RewriteResponseHeaders = rewriteHeadersFromPB(src.RewriteResponseHeaders) return diagnostics diff --git a/internal/provider/route_model_test.go b/internal/provider/route_model_test.go new file mode 100644 index 0000000..52ad10e --- /dev/null +++ b/internal/provider/route_model_test.go @@ -0,0 +1,85 @@ +package provider_test + +import ( + "context" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/pomerium/enterprise-client-go/pb" + "github.com/pomerium/enterprise-terraform-provider/internal/provider" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertRouteFromPB(t *testing.T) { + t.Run("jwt_issuer_format", func(t *testing.T) { + testCases := []struct { + name string + input pb.IssuerFormat + expected string + isNull bool + }{ + { + name: "host_only", + input: pb.IssuerFormat_IssuerHostOnly, + expected: "IssuerHostOnly", + }, + { + name: "uri", + input: pb.IssuerFormat_IssuerURI, + expected: "IssuerURI", + }, + { + name: "invalid value", + input: pb.IssuerFormat(999), + isNull: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &provider.RouteModel{} + r := &pb.Route{ + JwtIssuerFormat: tc.input, + } + diags := provider.ConvertRouteFromPB(m, r) + require.False(t, diags.HasError()) + if tc.isNull { + assert.True(t, m.JWTIssuerFormat.IsNull()) + } else { + assert.Equal(t, tc.expected, m.JWTIssuerFormat.ValueString()) + } + }) + } + }) +} + +func TestConvertRouteToPB(t *testing.T) { + t.Run("jwt_issuer_format", func(t *testing.T) { + testCases := []struct { + name string + input string + expected pb.IssuerFormat + expectError bool + }{ + {"host_only", "IssuerHostOnly", pb.IssuerFormat_IssuerHostOnly, false}, + {"uri", "IssuerURI", pb.IssuerFormat_IssuerURI, false}, + {"invalid_value", "invalid_value", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + m := &provider.RouteModel{ + JWTIssuerFormat: types.StringValue(tc.input), + } + r, diag := provider.ConvertRouteToPB(context.Background(), m) + if tc.expectError { + require.True(t, diag.HasError()) + } else { + require.False(t, diag.HasError()) + assert.Equal(t, tc.expected, r.JwtIssuerFormat) + } + }) + } + }) +} diff --git a/internal/provider/route_test.go b/internal/provider/route_test.go index 18fcf93..8d7af05 100644 --- a/internal/provider/route_test.go +++ b/internal/provider/route_test.go @@ -50,6 +50,7 @@ func TestConvertRoute(t *testing.T) { EnableGoogleCloudServerlessAuthentication: true, TlsCustomCaKeyPairId: P("custom-ca-1"), KubernetesServiceAccountTokenFile: P("/path/to/token"), + JwtIssuerFormat: pb.IssuerFormat_IssuerURI, } var actual provider.RouteResourceModel @@ -108,6 +109,7 @@ func TestConvertRoute(t *testing.T) { EnableGoogleCloudServerlessAuthentication: types.BoolValue(true), TLSCustomCAKeyPairID: types.StringValue("custom-ca-1"), KubernetesServiceAccountTokenFile: types.StringValue("/path/to/token"), + JWTIssuerFormat: types.StringValue("IssuerURI"), } if diff := cmp.Diff(expected, actual); diff != "" {