diff --git a/internal/functions/testdata/provider-function-zone-from-id.cassette.yaml b/internal/functions/testdata/provider-function-zone-from-id.cassette.yaml new file mode 100644 index 0000000000..2797c38e00 --- /dev/null +++ b/internal/functions/testdata/provider-function-zone-from-id.cassette.yaml @@ -0,0 +1,3 @@ +--- +version: 2 +interactions: [] diff --git a/internal/functions/zone_from_id.go b/internal/functions/zone_from_id.go new file mode 100644 index 0000000000..1e6cd0061e --- /dev/null +++ b/internal/functions/zone_from_id.go @@ -0,0 +1,86 @@ +package functions + +import ( + "context" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/scaleway/scaleway-sdk-go/scw" +) + +var _ function.Function = &ZoneFromID{} + +type ZoneFromID struct{} + +func NewZoneFromID() function.Function { + return &ZoneFromID{} +} + +func (f *ZoneFromID) Metadata(ctx context.Context, req function.MetadataRequest, resp *function.MetadataResponse) { + resp.Name = "zone_from_id" +} + +func (f *ZoneFromID) Definition(ctx context.Context, req function.DefinitionRequest, resp *function.DefinitionResponse) { + resp.Definition = function.Definition{ + Summary: "Extract a zone from the ID", + Description: "Given an ID string value, returns the zone contained in the ID.", + Parameters: []function.Parameter{ + function.StringParameter{ + Name: "id", + Description: "id to extract the zone from", + }, + function.BoolParameter{ + Name: "skip_zone_validation", + Description: "If true, will skip zone validation with the zone known by the Scaleway SDK.", + AllowNullValue: true, + }, + }, + Return: function.StringReturn{}, + } +} + +func (f *ZoneFromID) Run(ctx context.Context, req function.RunRequest, resp *function.RunResponse) { + var ( + input types.String + skipZoneValidation types.Bool + ) + + resp.Error = function.ConcatFuncErrors(resp.Error, req.Arguments.Get(ctx, &input, &skipZoneValidation)) + + if input.IsNull() || input.IsUnknown() { + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, input)) + + return + } + + if input.ValueString() == "" { + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(0, "bad zone format, available zones are: fr-par-1, nl-ams-1, pl-waw-1")) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringUnknown())) + + return + } + + if skipZoneValidation.IsNull() || skipZoneValidation.IsUnknown() { + skipZoneValidation = basetypes.NewBoolValue(false) + } + + idParts := strings.Split(input.ValueString(), "/") + if len(idParts) < 2 { + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(0, "cannot parse ID: invalid format")) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringUnknown())) + + return + } + + zone, err := scw.ParseZone(idParts[0]) + if err != nil && !skipZoneValidation.ValueBool() { + resp.Error = function.ConcatFuncErrors(resp.Error, function.NewArgumentFuncError(0, err.Error())) + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, basetypes.NewStringUnknown())) + + return + } + + resp.Error = function.ConcatFuncErrors(resp.Error, resp.Result.Set(ctx, types.StringValue(zone.String()))) +} diff --git a/internal/functions/zone_from_id_test.go b/internal/functions/zone_from_id_test.go new file mode 100644 index 0000000000..46529058b5 --- /dev/null +++ b/internal/functions/zone_from_id_test.go @@ -0,0 +1,154 @@ +package functions_test + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/function" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/acctest" + "github.com/scaleway/terraform-provider-scaleway/v2/internal/functions" +) + +func TestZoneFromIDFunctionRun(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + expected function.RunResponse + request function.RunRequest + }{ + // The example implementation uses the Go built-in string type, however + // if AllowNullValue was enabled and *string or types.String was used, + // this test case shows how the function would be expected to behave. + "null": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringNull(), types.BoolNull()}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringNull()), + }, + }, + // The example implementation uses the Go built-in string type, however + // if AllowUnknownValues was enabled and types.String was used, + // this test case shows how the function would be expected to behave. + "unknown": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringUnknown(), types.BoolNull()}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringUnknown()), + }, + }, + // Test valid ID format - extracts zone from ID + "valid-id-format": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("fr-par-1/1111-1111-1111-1111-1111111111111111"), types.BoolNull()}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue("fr-par-1")), + }, + }, + // Test another valid ID format + "valid-id-format-amsterdam": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("nl-ams-1/1111-1111-1111-1111-1111111111111111"), types.BoolNull()}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue("nl-ams-1")), + }, + }, + "valid-id-multi-part": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("nl-ams-1/foo/bar"), types.BoolNull()}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue("nl-ams-1")), + }, + }, + "unknown-id-valid-format": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("xx-yyy-1/11111111-1111-1111-1111-111111111111"), types.BoolValue(true)}), + }, + expected: function.RunResponse{ + Result: function.NewResultData(types.StringValue("xx-yyy-1")), + }, + }, + // Test invalid format - empty string + "empty-string": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue(""), types.BoolNull()}), + }, + expected: function.RunResponse{ + Error: function.NewArgumentFuncError(0, "bad zone format, available zones are: fr-par-1, nl-ams-1, pl-waw-1"), + Result: function.NewResultData(types.StringUnknown()), + }, + }, + // Test invalid format - malformed ID + "malformed-id": { + request: function.RunRequest{ + Arguments: function.NewArgumentsData([]attr.Value{types.StringValue("invalid-format"), types.BoolNull()}), + }, + expected: function.RunResponse{ + Error: function.NewArgumentFuncError(0, "cannot parse ID: invalid format"), + Result: function.NewResultData(types.StringUnknown()), + }, + }, + } + + for name, testCase := range testCases { + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := function.RunResponse{ + Result: function.NewResultData(types.StringUnknown()), + } + + functions.NewZoneFromID().Run(context.Background(), testCase.request, &got) + + if diff := cmp.Diff(got, testCase.expected); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestAccProviderFunction_Zone_From_ID(t *testing.T) { + tt := acctest.NewTestTools(t) + defer tt.Cleanup() + + resource.ParallelTest(t, resource.TestCase{ + ProtoV6ProviderFactories: tt.ProviderFactories, + Steps: []resource.TestStep{ + { + // Can get the zone from a resource's id in one step + Config: ` +# terraform block required for provider function to be found +erraform { + required_providers { + scaleway = { + source = "scaleway/scaleway" + } + } +} + +resource "scaleway_instance_server" "main" { + name = "terraform_test_zone_from_id" + type = "DEV1-S" + image = "fr-par-1/ubuntu_jammy" + zone = "fr-par-1" +} + +output "zone" { + value = provider::scaleway::zone_from_id(scaleway_instance_server.main.id) +} +`, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckOutput("zone", "fr-par-1"), + ), + }, + }, + }) +} diff --git a/provider/framework.go b/provider/framework.go index 76fbaae4c3..9d07504457 100644 --- a/provider/framework.go +++ b/provider/framework.go @@ -247,5 +247,6 @@ func (p *ScalewayProvider) ListResources(_ context.Context) []func() list.ListRe func (p *ScalewayProvider) Functions(_ context.Context) []func() function.Function { return []func() function.Function{ functions.NewRegionFromID, + functions.NewZoneFromID, } }