From e35a10ca21bc853c78bce4738ec8f1d1b231e2bc Mon Sep 17 00:00:00 2001 From: Heng Lu Date: Wed, 25 Feb 2026 14:02:41 +0800 Subject: [PATCH 1/2] Support import for msgraph_resource_collection --- CHANGELOG.md | 7 ++- docs/resources/resource_collection.md | 8 +++ .../msgraph_resource_collection/import.sh | 5 ++ .../services/msgraph_resource_collection.go | 50 +++++++++++++++++-- .../msgraph_resource_collection_test.go | 50 +++++++++++++++++++ 5 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 examples/resources/msgraph_resource_collection/import.sh diff --git a/CHANGELOG.md b/CHANGELOG.md index 53bbce4..60a11b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,9 @@ -## 0.3.0 (Unreleased) +## 0.4.0 (Unreleased) + +ENHANCEMENTS: +- `msgraph_resource_collection`: Support resource import for collection resources (e.g. groups/{id}/members/$ref) to allow importing existing relationships into Terraform state. + +## 0.3.0 FEATURES: - **New Authentication Method**: Azure PowerShell authentication support via `use_powershell` provider attribute diff --git a/docs/resources/resource_collection.md b/docs/resources/resource_collection.md index e64c78d..248674f 100644 --- a/docs/resources/resource_collection.md +++ b/docs/resources/resource_collection.md @@ -141,4 +141,12 @@ Optional: - `read` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). Read operations occur during any refresh or planning operation when refresh is enabled. - `update` (String) A string that can be [parsed as a duration](https://pkg.go.dev/time#ParseDuration) consisting of numbers and unit suffixes, such as "30s" or "2h45m". Valid time units are "s" (seconds), "m" (minutes), "h" (hours). +## Import + ```shell + # MSGraph resource collection can be imported using the collection $ref URL, e.g. + terraform import msgraph_resource_collection.group_members groups/00000000-0000-0000-0000-000000000000/members/$ref + + # To import using the beta API version, append the api-version query parameter: + terraform import msgraph_resource_collection.group_members 'groups/00000000-0000-0000-0000-000000000000/members/$ref?api-version=beta' + ``` diff --git a/examples/resources/msgraph_resource_collection/import.sh b/examples/resources/msgraph_resource_collection/import.sh new file mode 100644 index 0000000..cbe8db9 --- /dev/null +++ b/examples/resources/msgraph_resource_collection/import.sh @@ -0,0 +1,5 @@ +# MSGraph resource collection can be imported using the collection $ref URL, e.g. +terraform import msgraph_resource_collection.group_members groups/00000000-0000-0000-0000-000000000000/members/$ref + +# To import using the beta API version, append the api-version query parameter: +terraform import msgraph_resource_collection.group_members 'groups/00000000-0000-0000-0000-000000000000/members/$ref?api-version=beta' diff --git a/internal/services/msgraph_resource_collection.go b/internal/services/msgraph_resource_collection.go index 1e97d9c..fcf13f6 100644 --- a/internal/services/msgraph_resource_collection.go +++ b/internal/services/msgraph_resource_collection.go @@ -4,12 +4,14 @@ import ( "context" "encoding/json" "fmt" + "net/url" "reflect" "strings" "time" "github.com/hashicorp/terraform-plugin-framework-timeouts/resource/timeouts" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" "github.com/hashicorp/terraform-plugin-framework/resource" "github.com/hashicorp/terraform-plugin-framework/resource/schema" "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" @@ -27,9 +29,10 @@ import ( ) var ( - _ resource.Resource = &MSGraphResourceCollection{} - _ resource.ResourceWithConfigure = &MSGraphResourceCollection{} - _ resource.ResourceWithModifyPlan = &MSGraphResourceCollection{} + _ resource.Resource = &MSGraphResourceCollection{} + _ resource.ResourceWithConfigure = &MSGraphResourceCollection{} + _ resource.ResourceWithModifyPlan = &MSGraphResourceCollection{} + _ resource.ResourceWithImportState = &MSGraphResourceCollection{} ) func NewMSGraphResourceCollection() resource.Resource { @@ -337,4 +340,45 @@ func flattenReferenceIds(body interface{}) ([]string, error) { return result, nil } +func (r *MSGraphResourceCollection) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + parsedUrl, err := url.Parse(req.ID) + if err != nil { + resp.Diagnostics.AddError("Failed to parse URL", err.Error()) + return + } + + urlValue := strings.TrimPrefix(parsedUrl.Path, "/") + + if !strings.HasSuffix(urlValue, "/$ref") { + resp.Diagnostics.AddError( + "Invalid Import ID", + fmt.Sprintf("The import ID must be a collection URL ending with '/$ref'. For example: 'groups/{group-id}/members/$ref'. Got: %s", req.ID), + ) + return + } + + apiVersion := "v1.0" + if parsedUrl.Query().Get("api-version") != "" { + apiVersion = parsedUrl.Query().Get("api-version") + } + + model := &MSGraphResourceCollectionModel{ + Id: types.StringValue(baseCollectionUrl(urlValue)), + Url: types.StringValue(urlValue), + ApiVersion: types.StringValue(apiVersion), + ReferenceIds: types.ListNull(types.StringType), + ReadQueryParameters: types.MapNull(types.ListType{ElemType: types.StringType}), + Retry: retry.NewValueNull(), + Timeouts: timeouts.Value{ + Object: types.ObjectNull(map[string]attr.Type{ + "create": types.StringType, + "update": types.StringType, + "read": types.StringType, + "delete": types.StringType, + }), + }, + } + resp.Diagnostics.Append(resp.State.Set(ctx, model)...) +} + func baseCollectionUrl(url string) string { return strings.TrimSuffix(url, "/$ref") } diff --git a/internal/services/msgraph_resource_collection_test.go b/internal/services/msgraph_resource_collection_test.go index dd45fef..ea21517 100644 --- a/internal/services/msgraph_resource_collection_test.go +++ b/internal/services/msgraph_resource_collection_test.go @@ -30,6 +30,7 @@ func TestAcc_ResourceCollectionBasic(t *testing.T) { resource.TestCheckResourceAttr(data.ResourceName, "reference_ids.#", "1"), ), }, + data.ImportStepWithImportStateIdFunc(r.ImportIdFunc), }) } @@ -46,6 +47,7 @@ func TestAcc_ResourceCollectionUpdate(t *testing.T) { resource.TestCheckResourceAttr(data.ResourceName, "reference_ids.#", "1"), ), }, + data.ImportStepWithImportStateIdFunc(r.ImportIdFunc), { // add second member Config: r.updateTwoMembers(), @@ -54,6 +56,7 @@ func TestAcc_ResourceCollectionUpdate(t *testing.T) { resource.TestCheckResourceAttr(data.ResourceName, "reference_ids.#", "2"), ), }, + data.ImportStepWithImportStateIdFunc(r.ImportIdFunc, "reference_ids.#", "reference_ids.0", "reference_ids.1"), { // remove second member again Config: r.updateOneMember(), @@ -65,6 +68,38 @@ func TestAcc_ResourceCollectionUpdate(t *testing.T) { }) } +func TestAcc_ResourceCollectionImport(t *testing.T) { + data := acceptance.BuildTestData(t, "msgraph_resource_collection", "test") + r := MSGraphTestResourceCollection{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Exists(r), + resource.TestCheckResourceAttr(data.ResourceName, "reference_ids.#", "1"), + ), + }, + data.ImportStepWithImportStateIdFunc(r.ImportIdFunc), + }) +} + +func TestAcc_ResourceCollectionImportWithApiVersion(t *testing.T) { + data := acceptance.BuildTestData(t, "msgraph_resource_collection", "test") + r := MSGraphTestResourceCollection{} + + data.ResourceTest(t, r, []resource.TestStep{ + { + Config: r.basic(), + Check: resource.ComposeTestCheckFunc( + check.That(data.ResourceName).Exists(r), + resource.TestCheckResourceAttr(data.ResourceName, "reference_ids.#", "1"), + ), + }, + data.ImportStepWithImportStateIdFunc(r.ImportIdFuncWithBetaApiVersion), + }) +} + func TestAcc_ResourceCollectionRetry(t *testing.T) { data := acceptance.BuildTestData(t, "msgraph_resource_collection", "test") r := MSGraphTestResourceCollection{} @@ -156,6 +191,21 @@ func (r MSGraphTestResourceCollection) Exists(ctx context.Context, client *clien return nil, fmt.Errorf("checking for presence of existing collection %s(api_version=%s): %w", id, apiVersion, err) } +func (r MSGraphTestResourceCollection) ImportIdFunc(tfState *terraform.State) (string, error) { + state := tfState.RootModule().Resources["msgraph_resource_collection.test"].Primary + url := state.Attributes["url"] + apiVersion := state.Attributes["api_version"] + if apiVersion != "" && apiVersion != "v1.0" { + return url + "?api-version=" + apiVersion, nil + } + return url, nil +} + +func (r MSGraphTestResourceCollection) ImportIdFuncWithBetaApiVersion(tfState *terraform.State) (string, error) { + state := tfState.RootModule().Resources["msgraph_resource_collection.test"].Primary + return state.Attributes["url"] + "?api-version=beta", nil +} + // configuration helpers func (r MSGraphTestResourceCollection) basic() string { return ` From e604b6dfc3eacc29892566aa0caf4204d6f50fce Mon Sep 17 00:00:00 2001 From: Heng Lu Date: Wed, 25 Feb 2026 14:10:21 +0800 Subject: [PATCH 2/2] fix sec --- internal/provider/auth_oidc.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/provider/auth_oidc.go b/internal/provider/auth_oidc.go index 1398cd3..56809a3 100644 --- a/internal/provider/auth_oidc.go +++ b/internal/provider/auth_oidc.go @@ -91,6 +91,7 @@ func (w *OidcCredential) getAssertion(ctx context.Context) (string, error) { req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.requestToken)) req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + // #nosec G704 resp, err := http.DefaultClient.Do(req) if err != nil { return "", fmt.Errorf("getAssertion: cannot request token: %v", err)