diff --git a/internal/services/synchronization/client/client.go b/internal/services/synchronization/client/client.go index 5ba6612af6..cfbbb00489 100644 --- a/internal/services/synchronization/client/client.go +++ b/internal/services/synchronization/client/client.go @@ -4,27 +4,40 @@ package client import ( + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/serviceprincipal" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationjob" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationsecret" "github.com/hashicorp/terraform-provider-azuread/internal/common" - "github.com/manicminer/hamilton/msgraph" ) type Client struct { - ServicePrincipalsClient *msgraph.ServicePrincipalsClient - SynchronizationJobClient *msgraph.SynchronizationJobClient + ServicePrincipalClient *serviceprincipal.ServicePrincipalClient + SynchronizationJobClient *synchronizationjob.SynchronizationJobClient + SynchronizationSecretClient *synchronizationsecret.SynchronizationSecretClient } -func NewClient(o *common.ClientOptions) *Client { - servicePrincipalsClient := msgraph.NewServicePrincipalsClient() - o.ConfigureClient(&servicePrincipalsClient.BaseClient) +func NewClient(o *common.ClientOptions) (*Client, error) { + servicePrincipalClient, err := serviceprincipal.NewServicePrincipalClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(servicePrincipalClient.Client) - synchronizationJobClient := msgraph.NewSynchronizationJobClient() - o.ConfigureClient(&synchronizationJobClient.BaseClient) + synchronizationJobClient, err := synchronizationjob.NewSynchronizationJobClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(synchronizationJobClient.Client) - // Synchronization doesn't yet exist in v1.0 - synchronizationJobClient.BaseClient.ApiVersion = msgraph.VersionBeta + synchronizationSecretClient, err := synchronizationsecret.NewSynchronizationSecretClientWithBaseURI(o.Environment.MicrosoftGraph) + if err != nil { + return nil, err + } + o.Configure(synchronizationSecretClient.Client) return &Client{ - ServicePrincipalsClient: servicePrincipalsClient, - SynchronizationJobClient: synchronizationJobClient, - } + ServicePrincipalClient: servicePrincipalClient, + SynchronizationJobClient: synchronizationJobClient, + SynchronizationSecretClient: synchronizationSecretClient, + }, nil } diff --git a/internal/services/synchronization/registration.go b/internal/services/synchronization/registration.go index 57b805324d..1bd8fecc59 100644 --- a/internal/services/synchronization/registration.go +++ b/internal/services/synchronization/registration.go @@ -4,8 +4,8 @@ package synchronization import ( + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" "github.com/hashicorp/terraform-provider-azuread/internal/sdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" ) type Registration struct{} diff --git a/internal/services/synchronization/synchronization.go b/internal/services/synchronization/synchronization.go index a409c3158f..a9d0620883 100644 --- a/internal/services/synchronization/synchronization.go +++ b/internal/services/synchronization/synchronization.go @@ -4,16 +4,26 @@ package synchronization import ( - "time" + "net/http" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/sdk/client" + "github.com/hashicorp/go-azure-sdk/sdk/nullable" + "github.com/hashicorp/go-azure-sdk/sdk/odata" ) const servicePrincipalResourceName = "azuread_service_principal" -func emptySynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph.SynchronizationSecretKeyStringValuePair { - result := make([]msgraph.SynchronizationSecretKeyStringValuePair, 0) +func synchronizationRetryFunc() client.RequestRetryFunc { + return func(resp *http.Response, o *odata.OData) (bool, error) { + return response.WasConflict(resp) || response.WasStatusCode(resp, http.StatusForbidden), nil + } +} + +func emptySynchronizationSecretKeyStringValuePair(in []interface{}) *[]stable.SynchronizationSecretKeyStringValuePair { + result := make([]stable.SynchronizationSecretKeyStringValuePair, 0) for _, raw := range in { if raw == nil { @@ -21,17 +31,17 @@ func emptySynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph.S } item := raw.(map[string]interface{}) - result = append(result, msgraph.SynchronizationSecretKeyStringValuePair{ - Key: pointer.To(item["key"].(string)), - Value: pointer.To(""), + result = append(result, stable.SynchronizationSecretKeyStringValuePair{ + Key: pointer.To(stable.SynchronizationSecret(item["key"].(string))), + Value: nullable.Value(""), }) } return &result } -func expandSynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph.SynchronizationSecretKeyStringValuePair { - result := make([]msgraph.SynchronizationSecretKeyStringValuePair, 0) +func expandSynchronizationSecretKeyStringValuePair(in []interface{}) *[]stable.SynchronizationSecretKeyStringValuePair { + result := make([]stable.SynchronizationSecretKeyStringValuePair, 0) for _, raw := range in { if raw == nil { @@ -39,17 +49,17 @@ func expandSynchronizationSecretKeyStringValuePair(in []interface{}) *[]msgraph. } item := raw.(map[string]interface{}) - result = append(result, msgraph.SynchronizationSecretKeyStringValuePair{ - Key: pointer.To(item["key"].(string)), - Value: pointer.To(item["value"].(string)), + result = append(result, stable.SynchronizationSecretKeyStringValuePair{ + Key: pointer.To(stable.SynchronizationSecret(item["key"].(string))), + Value: nullable.Value(item["value"].(string)), }) } return &result } -func expandSynchronizationJobApplicationParameters(in []interface{}) *[]msgraph.SynchronizationJobApplicationParameters { - result := make([]msgraph.SynchronizationJobApplicationParameters, 0) +func expandSynchronizationJobApplicationParameters(in []interface{}) *[]stable.SynchronizationJobApplicationParameters { + result := make([]stable.SynchronizationJobApplicationParameters, 0) for _, raw := range in { if raw == nil { @@ -57,70 +67,66 @@ func expandSynchronizationJobApplicationParameters(in []interface{}) *[]msgraph. } item := raw.(map[string]interface{}) - result = append(result, msgraph.SynchronizationJobApplicationParameters{ + result = append(result, stable.SynchronizationJobApplicationParameters{ Subjects: expandSynchronizationJobSubject(item["subject"].([]interface{})), - RuleId: pointer.To(item["rule_id"].(string)), + RuleId: nullable.Value(item["rule_id"].(string)), }) } return &result } -func expandSynchronizationJobSubject(in []interface{}) *[]msgraph.SynchronizationJobSubject { - result := make([]msgraph.SynchronizationJobSubject, 0) +func expandSynchronizationJobSubject(in []interface{}) *[]stable.SynchronizationJobSubject { + result := make([]stable.SynchronizationJobSubject, 0) for _, raw := range in { if raw == nil { continue } item := raw.(map[string]interface{}) - result = append(result, msgraph.SynchronizationJobSubject{ - ObjectId: pointer.To(item["object_id"].(string)), - ObjectTypeName: pointer.To(item["object_type_name"].(string)), + result = append(result, stable.SynchronizationJobSubject{ + ObjectId: nullable.Value(item["object_id"].(string)), + ObjectTypeName: nullable.Value(item["object_type_name"].(string)), }) } return &result } -func flattenSynchronizationSchedule(in *msgraph.SynchronizationSchedule) []map[string]interface{} { +func flattenSynchronizationSchedule(in *stable.SynchronizationSchedule) []map[string]interface{} { if in == nil { return []map[string]interface{}{} } - expiration := "" - if v := in.Expiration; v != nil { - expiration = v.Format(time.RFC3339) - } return []map[string]interface{}{{ - "expiration": expiration, - "interval": in.Interval, - "state": in.State, + "expiration": in.Expiration.GetOrZero(), + "interval": pointer.From(in.Interval), + "state": pointer.From(in.State), }} } -func flattenSynchronizationSecretKeyStringValuePair(in *[]msgraph.SynchronizationSecretKeyStringValuePair, current []interface{}) []interface{} { +func flattenSynchronizationSecretKeyStringValuePair(in *[]stable.SynchronizationSecretKeyStringValuePair, current []interface{}) []interface{} { if in == nil { return []interface{}{} } credentials := make([]interface{}, 0) for _, item := range *in { - value := item.Value - if *value == "*" && current != nil { + value := item.Value.GetOrZero() + if value == "*" && current != nil { // Use value from state if API returns * indicating sensitive data for _, raw := range current { if raw == nil { continue } currentItem := raw.(map[string]interface{}) - if currentItem["key"].(string) == *item.Key { - value = pointer.To(currentItem["value"].(string)) + if currentItem["key"].(string) == string(pointer.From(item.Key)) { + value = currentItem["value"].(string) } } } credential := map[string]interface{}{ - "key": item.Key, + "key": pointer.From(item.Key), "value": value, } credentials = append(credentials, credential) @@ -129,13 +135,14 @@ func flattenSynchronizationSecretKeyStringValuePair(in *[]msgraph.Synchronizatio return credentials } -func allCredentialsRemoved(in []msgraph.SynchronizationSecretKeyStringValuePair, current []msgraph.SynchronizationSecretKeyStringValuePair) bool { +func allCredentialsRemoved(in []stable.SynchronizationSecretKeyStringValuePair, current []stable.SynchronizationSecretKeyStringValuePair) bool { for _, item := range in { for _, itemCurrent := range current { - if *item.Key == *itemCurrent.Key { + if pointer.From(item.Key) == pointer.From(itemCurrent.Key) { return false } } } + return true } diff --git a/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go b/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go index 3cb92c64d0..30d8c7c19c 100644 --- a/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go +++ b/internal/services/synchronization/synchronization_job_provision_on_demand_resource.go @@ -6,17 +6,18 @@ package synchronization import ( "context" "errors" - "net/http" "time" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/serviceprincipal" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationjob" "github.com/hashicorp/go-uuid" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" ) func synchronizationJobProvisionOnDemandResource() *schema.Resource { @@ -34,11 +35,11 @@ func synchronizationJobProvisionOnDemandResource() *schema.Resource { Schema: map[string]*schema.Schema{ "service_principal_id": { - Description: "The object ID of the service principal for which this synchronization job should be provisioned", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Description: "The object ID of the service principal for which this synchronization job should be provisioned", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, }, "synchronization_job_id": { @@ -102,43 +103,53 @@ func synchronizationJobProvisionOnDemandResource() *schema.Resource { func synchronizationProvisionOnDemandResourceCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client).ServicePrincipals.SynchronizationJobClient - spClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalsClient - objectId := d.Get("service_principal_id").(string) - jobId := d.Get("synchronization_job_id").(string) + servicePrincipalClient := meta.(*clients.Client).ServicePrincipals.ServicePrincipalClient - tf.LockByName(servicePrincipalResourceName, objectId) - defer tf.UnlockByName(servicePrincipalResourceName, objectId) + servicePrincipalId := stable.NewServicePrincipalID(d.Get("service_principal_id").(string)) - servicePrincipal, status, err := spClient.Get(ctx, objectId, odata.Query{}) + tf.LockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + defer tf.UnlockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + + servicePrincipalResp, err := servicePrincipalClient.GetServicePrincipal(ctx, servicePrincipalId, serviceprincipal.DefaultGetServicePrincipalOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", objectId) + if response.WasNotFound(servicePrincipalResp.HttpResponse) { + return tf.ErrorDiagPathF(nil, "service_principal_id", "%s was not found", servicePrincipalId) } - return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", objectId) + return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving %s", servicePrincipalId) + } + + servicePrincipal := servicePrincipalResp.Model + if servicePrincipal == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving %s", servicePrincipalId) } - if servicePrincipal == nil || servicePrincipal.ID() == nil { - return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", objectId) + if servicePrincipal.Id == nil { + return tf.ErrorDiagF(errors.New("model has nil ID"), "Retrieving %s", servicePrincipalId) } - job, status, err := client.Get(ctx, jobId, objectId) + jobId := stable.NewServicePrincipalIdSynchronizationJobID(servicePrincipalId.ServicePrincipalId, d.Get("synchronization_job_id").(string)) + + jobResp, err := client.GetSynchronizationJob(ctx, jobId, synchronizationjob.GetSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "job_id", "Job with object ID %q was not found for service principle %q", jobId, objectId) + if response.WasNotFound(jobResp.HttpResponse) { + return tf.ErrorDiagPathF(nil, "synchronization_job_id", "%s was not found", jobId) } - return tf.ErrorDiagPathF(err, "job_id", "Retrieving job with object ID %q for service principle %q", jobId, objectId) + return tf.ErrorDiagPathF(err, "job_id", "Retrieving %s", jobId) } - if job == nil || job.ID == nil { - return tf.ErrorDiagF(errors.New("nil job or job with nil ID was returned"), "API error retrieving job with object ID %q/%s", objectId, jobId) + + job := jobResp.Model + if job == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving %s", jobId) + } + if job.Id == nil { + return tf.ErrorDiagF(errors.New("model has nil ID"), "Retrieving %s", jobId) } - // Create a new synchronization job - synchronizationProvisionOnDemand := &msgraph.SynchronizationJobProvisionOnDemand{ + properties := synchronizationjob.ProvisionSynchronizationJobOnDemandRequest{ Parameters: expandSynchronizationJobApplicationParameters(d.Get("parameter").([]interface{})), } - _, err = client.ProvisionOnDemand(ctx, jobId, synchronizationProvisionOnDemand, *servicePrincipal.ID()) - if err != nil { - return tf.ErrorDiagF(err, "Creating synchronization job for service principal ID %q", *servicePrincipal.ID()) + if _, err = client.ProvisionSynchronizationJobOnDemand(ctx, jobId, properties, synchronizationjob.DefaultProvisionSynchronizationJobOnDemandOperationOptions()); err != nil { + return tf.ErrorDiagF(err, "Provisioning %s", jobId) } id, _ := uuid.GenerateUUID() @@ -147,10 +158,12 @@ func synchronizationProvisionOnDemandResourceCreate(ctx context.Context, d *sche return synchronizationProvisionOnDemandResourceRead(ctx, d, meta) } -func synchronizationProvisionOnDemandResourceRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func synchronizationProvisionOnDemandResourceRead(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + // Nothing to read return nil } -func synchronizationProvisionOnDemandResourceDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func synchronizationProvisionOnDemandResourceDelete(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + // Nothing to destroy return nil } diff --git a/internal/services/synchronization/synchronization_job_provision_on_demand_resource_test.go b/internal/services/synchronization/synchronization_job_provision_on_demand_resource_test.go index 1093e9b2bb..416e842f73 100644 --- a/internal/services/synchronization/synchronization_job_provision_on_demand_resource_test.go +++ b/internal/services/synchronization/synchronization_job_provision_on_demand_resource_test.go @@ -31,6 +31,7 @@ func TestAccSynchronizationJobProvisionOnDemand_basic(t *testing.T) { } func (r SynchronizationJobProvisionOnDemandResource) Exists(_ context.Context, _ *clients.Client, _ *terraform.InstanceState) (*bool, error) { + // Nothing to read return pointer.To(true), nil } @@ -44,20 +45,13 @@ data "azuread_application_template" "test" { display_name = "Azure Databricks SCIM Provisioning Connector" } -resource "azuread_application" "test" { +resource "azuread_application_from_template" "test" { display_name = "acctestSynchronizationJob-%[1]d" - owners = [data.azuread_client_config.test.object_id] template_id = data.azuread_application_template.test.template_id } -resource "azuread_service_principal" "test" { - client_id = azuread_application.test.client_id - owners = [data.azuread_client_config.test.object_id] - use_existing = true -} - resource "azuread_synchronization_job" "test" { - service_principal_id = azuread_service_principal.test.id + service_principal_id = azuread_application_from_template.test.service_principal_object_id template_id = "dataBricks" } @@ -73,8 +67,8 @@ func (r SynchronizationJobProvisionOnDemandResource) basic(data acceptance.TestD %[1]s resource "azuread_synchronization_job_provision_on_demand" "test" { - service_principal_id = azuread_service_principal.test.id - synchronization_job_id = trimprefix(azuread_synchronization_job.test.id, "${azuread_service_principal.test.id}/job/") + service_principal_id = azuread_application_from_template.test.service_principal_object_id + synchronization_job_id = trimprefix(azuread_synchronization_job.test.id, "${azuread_application_from_template.test.service_principal_object_id}/job/") parameter { rule_id = "03f7d90d-bf71-41b1-bda6-aaf0ddbee5d8" // appears to be a global value diff --git a/internal/services/synchronization/synchronization_job_resource.go b/internal/services/synchronization/synchronization_job_resource.go index 4206468c71..2b98c82957 100644 --- a/internal/services/synchronization/synchronization_job_resource.go +++ b/internal/services/synchronization/synchronization_job_resource.go @@ -8,18 +8,20 @@ import ( "errors" "fmt" "log" - "net/http" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/serviceprincipal" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationjob" + "github.com/hashicorp/go-azure-sdk/sdk/nullable" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" "github.com/hashicorp/terraform-provider-azuread/internal/services/synchronization/parse" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" ) func synchronizationJobResource() *pluginsdk.Resource { @@ -45,24 +47,27 @@ func synchronizationJobResource() *pluginsdk.Resource { Schema: map[string]*pluginsdk.Schema{ "service_principal_id": { - Description: "The object ID of the service principal for which this synchronization job should be created", - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Description: "The object ID of the service principal for which this synchronization job should be created", + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, }, + "template_id": { Description: "Identifier of the synchronization template this job is based on.", Type: pluginsdk.TypeString, Required: true, ForceNew: true, }, + "enabled": { Description: "Whether or not the synchronization job is enabled", Type: pluginsdk.TypeBool, Default: true, Optional: true, }, + "schedule": { Type: pluginsdk.TypeList, Computed: true, @@ -73,11 +78,13 @@ func synchronizationJobResource() *pluginsdk.Resource { Type: pluginsdk.TypeString, Computed: true, }, + "interval": { Description: "The interval between synchronization iterations ISO8601. E.g. PT40M run every 40 minutes.", Type: pluginsdk.TypeString, Computed: true, }, + "state": { Description: "State.", Type: pluginsdk.TypeString, @@ -92,72 +99,64 @@ func synchronizationJobResource() *pluginsdk.Resource { func synchronizationJobResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - spClient := meta.(*clients.Client).Synchronization.ServicePrincipalsClient - objectId := d.Get("service_principal_id").(string) + servicePrincipalClient := meta.(*clients.Client).Synchronization.ServicePrincipalClient + + servicePrincipalId := stable.NewServicePrincipalID(d.Get("service_principal_id").(string)) - tf.LockByName(servicePrincipalResourceName, objectId) - defer tf.UnlockByName(servicePrincipalResourceName, objectId) + tf.LockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + defer tf.UnlockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) - servicePrincipal, status, err := spClient.Get(ctx, objectId, odata.Query{}) + servicePrincipalResp, err := servicePrincipalClient.GetServicePrincipal(ctx, servicePrincipalId, serviceprincipal.DefaultGetServicePrincipalOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", objectId) + if response.WasNotFound(servicePrincipalResp.HttpResponse) { + return tf.ErrorDiagPathF(nil, "service_principal_id", "%s was not found", servicePrincipalId) } - return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", objectId) + return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving %s", servicePrincipalId) } - if servicePrincipal == nil || servicePrincipal.ID() == nil { - return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", objectId) + + servicePrincipal := servicePrincipalResp.Model + if servicePrincipal == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving %s", servicePrincipalId) } - // Create a new synchronization job - synchronizationJob := msgraph.SynchronizationJob{ - TemplateId: pointer.To(d.Get("template_id").(string)), + synchronizationJob := stable.SynchronizationJob{ + TemplateId: nullable.Value(d.Get("template_id").(string)), } - newJob, _, err := client.Create(ctx, synchronizationJob, *servicePrincipal.ID()) + resp, err := client.CreateSynchronizationJob(ctx, servicePrincipalId, synchronizationJob, synchronizationjob.CreateSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}) if err != nil { - return tf.ErrorDiagF(err, "Creating synchronization job for service principal ID %q", *servicePrincipal.ID()) + return tf.ErrorDiagF(err, "Creating synchronization job for %s", servicePrincipalId) } - if newJob == nil { - return tf.ErrorDiagF(errors.New("nil received when creating synchronization Job"), "API error creating synchronization job for service principal ID %q", *servicePrincipal.ID()) + if resp.Model == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "API error creating synchronization job for %s", servicePrincipalId) } - if newJob.ID == nil { - return tf.ErrorDiagF(errors.New("nil or empty id received"), "API error creating synchronization job for service principal ID %q", *servicePrincipal.ID()) + if resp.Model.Id == nil { + return tf.ErrorDiagF(errors.New("nil or empty id received"), "API error creating synchronization job for %s", servicePrincipalId) } - id := parse.NewSynchronizationJobID(*servicePrincipal.ID(), *newJob.ID) + id := stable.NewServicePrincipalIdSynchronizationJobID(servicePrincipalId.ServicePrincipalId, *resp.Model.Id) // Wait for the job to appear, this can take several moments - timeout, _ := ctx.Deadline() - _, err = (&pluginsdk.StateChangeConf{ //nolint:staticcheck - Pending: []string{"Waiting"}, - Target: []string{"Done"}, - Timeout: time.Until(timeout), - MinTimeout: 1 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - _, status, err := client.Get(ctx, id.JobId, id.ServicePrincipalId) - if err != nil { - if status == http.StatusNotFound { - return "stub", "Waiting", nil - } - return nil, "Error", fmt.Errorf("retrieving synchronization job") + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.GetSynchronizationJob(ctx, id, synchronizationjob.DefaultGetSynchronizationJobOperationOptions()) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil } - return "stub", "Done", nil - }, - }).WaitForStateContext(ctx) - - if err != nil { - return tf.ErrorDiagF(err, "Waiting for synchronization job %q", id.JobId) + return pointer.To(false), fmt.Errorf("retrieving synchronization job") + } + return pointer.To(true), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for creation of %s", id) } - d.SetId(id.String()) + resourceId := parse.NewSynchronizationJobID(id.ServicePrincipalId, id.SynchronizationJobId) + d.SetId(resourceId.String()) // Start job if desired if d.Get("enabled").(bool) { - _, err := client.Start(ctx, id.JobId, id.ServicePrincipalId) - if err != nil { - return tf.ErrorDiagF(err, "Starting synchronization job %q", id.JobId) + if _, err = client.StartSynchronizationJob(ctx, id, synchronizationjob.StartSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Starting %s", id) } } @@ -167,77 +166,89 @@ func synchronizationJobResourceCreate(ctx context.Context, d *pluginsdk.Resource func synchronizationJobResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - id, err := parse.SynchronizationJobID(d.Id()) + resourceId, err := parse.SynchronizationJobID(d.Id()) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Parsing synchronization job with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Parsing synchronization job ID %q", d.Id()) } - job, status, err := client.Get(ctx, id.JobId, id.ServicePrincipalId) + id := stable.NewServicePrincipalIdSynchronizationJobID(resourceId.ServicePrincipalId, resourceId.JobId) + + resp, err := client.GetSynchronizationJob(ctx, id, synchronizationjob.GetSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}) if err != nil { - if status == http.StatusNotFound { - log.Printf("[DEBUG] Synchronization job with ID %q for service principal %q was not found - removing from state!", id.JobId, id.ServicePrincipalId) + if response.WasNotFound(resp.HttpResponse) { + log.Printf("[DEBUG] %s was not found - removing from state!", id) d.SetId("") return nil } - return tf.ErrorDiagF(err, "Retrieving synchronization job with object ID %q", id.JobId) + return tf.ErrorDiagF(err, "Retrieving %s", id) + } + + synchronizationJob := resp.Model + if synchronizationJob == nil { + return tf.ErrorDiagF(errors.New("model was nil"), "Retrieving %s", id) } + tf.Set(d, "service_principal_id", id.ServicePrincipalId) - tf.Set(d, "schedule", flattenSynchronizationSchedule(job.Schedule)) - tf.Set(d, "template_id", job.TemplateId) - tf.Set(d, "enabled", *job.Schedule.State == "Active") + tf.Set(d, "schedule", flattenSynchronizationSchedule(synchronizationJob.Schedule)) + tf.Set(d, "template_id", synchronizationJob.TemplateId.GetOrZero()) + tf.Set(d, "enabled", pointer.From(synchronizationJob.Schedule.State) == stable.SynchronizationScheduleState_Active) return nil } func synchronizationJobResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - id, err := parse.SynchronizationJobID(d.Id()) + + resourceId, err := parse.SynchronizationJobID(d.Id()) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Parsing synchronization job with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Parsing synchronization job ID %q", d.Id()) } + + id := stable.NewServicePrincipalIdSynchronizationJobID(resourceId.ServicePrincipalId, resourceId.JobId) + if d.HasChange("enabled") { if d.Get("enabled").(bool) { - _, err := client.Start(ctx, id.JobId, id.ServicePrincipalId) - if err != nil { - return tf.ErrorDiagF(err, "Starting synchronization job %q", id.JobId) + if _, err = client.StartSynchronizationJob(ctx, id, synchronizationjob.StartSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Starting %s", id) } } else { - _, err := client.Pause(ctx, id.JobId, id.ServicePrincipalId) - if err != nil { - return tf.ErrorDiagF(err, "Pausing synchronization job %q", id.JobId) + if _, err = client.PauseSynchronizationJob(ctx, id, synchronizationjob.PauseSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Pausing %s", id) } } } + return synchronizationJobResourceRead(ctx, d, meta) } func synchronizationJobResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - id, err := parse.SynchronizationJobID(d.Id()) + resourceId, err := parse.SynchronizationJobID(d.Id()) if err != nil { return tf.ErrorDiagPathF(err, "id", "Parsing synchronization job with ID %q", d.Id()) } + id := stable.NewServicePrincipalIdSynchronizationJobID(resourceId.ServicePrincipalId, resourceId.JobId) + tf.LockByName(servicePrincipalResourceName, id.ServicePrincipalId) defer tf.UnlockByName(servicePrincipalResourceName, id.ServicePrincipalId) - if _, err := client.Delete(ctx, id.JobId, id.ServicePrincipalId); err != nil { - return tf.ErrorDiagF(err, "Removing job %q from service principal with object ID %q", id.JobId, id.ServicePrincipalId) + if _, err = client.DeleteSynchronizationJob(ctx, id, synchronizationjob.DeleteSynchronizationJobOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Removing %s", id) } // Wait for synchronization job to be deleted - if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true - - job, _, _ := client.Get(ctx, id.JobId, id.ServicePrincipalId) - if job == nil { - return pointer.To(false), nil + if err = consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.GetSynchronizationJob(ctx, id, synchronizationjob.DefaultGetSynchronizationJobOperationOptions()) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil + } + return nil, err } - return pointer.To(true), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for deletion of synchronization job %q from service principal with object ID %q", id.JobId, id.ServicePrincipalId) + return tf.ErrorDiagF(err, "Waiting for deletion of %s", id) } return nil diff --git a/internal/services/synchronization/synchronization_job_resource_test.go b/internal/services/synchronization/synchronization_job_resource_test.go index 446ffdbe96..84ea3ec976 100644 --- a/internal/services/synchronization/synchronization_job_resource_test.go +++ b/internal/services/synchronization/synchronization_job_resource_test.go @@ -6,10 +6,12 @@ package synchronization_test import ( "context" "fmt" - "net/http" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationjob" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -64,21 +66,22 @@ func testAccSynchronizationJob_disabled(t *testing.T) { func (r SynchronizationJobResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { client := clients.ServicePrincipals.SynchronizationJobClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() - id, err := parse.SynchronizationJobID(state.ID) + resourceId, err := parse.SynchronizationJobID(state.ID) if err != nil { return nil, fmt.Errorf("parsing synchronization job ID: %v", err) } - _, status, err := client.Get(ctx, id.JobId, id.ServicePrincipalId) + id := stable.NewServicePrincipalIdSynchronizationJobID(resourceId.ServicePrincipalId, resourceId.JobId) + + resp, err := client.GetSynchronizationJob(ctx, id, synchronizationjob.DefaultGetSynchronizationJobOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return nil, fmt.Errorf("Synchronization job %q was not found for service principal %q", id.JobId, id.ServicePrincipalId) + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil } - return nil, fmt.Errorf("Retrieving synchronization job with object ID %q", id.JobId) + return nil, fmt.Errorf("retrieving %s", id) } + return pointer.To(true), nil } @@ -92,17 +95,10 @@ data "azuread_application_template" "test" { display_name = "Azure Databricks SCIM Provisioning Connector" } -resource "azuread_application" "test" { +resource "azuread_application_from_template" "test" { display_name = "acctestSynchronizationJob-%[1]d" - owners = [data.azuread_client_config.test.object_id] template_id = data.azuread_application_template.test.template_id } - -resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id - owners = [data.azuread_client_config.test.object_id] - use_existing = true -} `, data.RandomInteger) } @@ -111,7 +107,7 @@ func (r SynchronizationJobResource) basic(data acceptance.TestData) string { %[1]s resource "azuread_synchronization_job" "test" { - service_principal_id = azuread_service_principal.test.id + service_principal_id = azuread_application_from_template.test.service_principal_object_id template_id = "dataBricks" } `, r.template(data)) @@ -122,7 +118,7 @@ func (r SynchronizationJobResource) disabled(data acceptance.TestData) string { %[1]s resource "azuread_synchronization_job" "test" { - service_principal_id = azuread_service_principal.test.id + service_principal_id = azuread_application_from_template.test.service_principal_object_id template_id = "dataBricks" enabled = false } diff --git a/internal/services/synchronization/synchronization_secret_resource.go b/internal/services/synchronization/synchronization_secret_resource.go index 5041c1ace3..a6723647be 100644 --- a/internal/services/synchronization/synchronization_secret_resource.go +++ b/internal/services/synchronization/synchronization_secret_resource.go @@ -5,21 +5,20 @@ package synchronization import ( "context" - "errors" "fmt" "log" - "net/http" "time" "github.com/hashicorp/go-azure-helpers/lang/pointer" - "github.com/hashicorp/go-azure-sdk/sdk/odata" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationsecret" "github.com/hashicorp/terraform-provider-azuread/internal/clients" - "github.com/hashicorp/terraform-provider-azuread/internal/helpers" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/consistency" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/pluginsdk" + "github.com/hashicorp/terraform-provider-azuread/internal/helpers/tf/validation" "github.com/hashicorp/terraform-provider-azuread/internal/services/synchronization/parse" - "github.com/hashicorp/terraform-provider-azuread/internal/tf" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/pluginsdk" - "github.com/hashicorp/terraform-provider-azuread/internal/tf/validation" - "github.com/manicminer/hamilton/msgraph" ) func synchronizationSecretResource() *pluginsdk.Resource { @@ -38,12 +37,13 @@ func synchronizationSecretResource() *pluginsdk.Resource { Schema: map[string]*pluginsdk.Schema{ "service_principal_id": { - Description: "The object ID of the service principal for which this synchronization secret should be created", - Type: pluginsdk.TypeString, - Required: true, - ForceNew: true, - ValidateDiagFunc: validation.ValidateDiag(validation.IsUUID), + Description: "The object ID of the service principal for which this synchronization secret should be created", + Type: pluginsdk.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsUUID, }, + "credential": { Type: pluginsdk.TypeList, Optional: true, @@ -68,145 +68,167 @@ func synchronizationSecretResource() *pluginsdk.Resource { } func synchronizationSecretResourceCreate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - spClient := meta.(*clients.Client).Synchronization.ServicePrincipalsClient - objectId := d.Get("service_principal_id").(string) + client := meta.(*clients.Client).Synchronization.SynchronizationSecretClient - tf.LockByName(servicePrincipalResourceName, objectId) - defer tf.UnlockByName(servicePrincipalResourceName, objectId) + servicePrincipalId := stable.NewServicePrincipalID(d.Get("service_principal_id").(string)) - servicePrincipal, status, err := spClient.Get(ctx, objectId, odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", objectId) - } - return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", objectId) + tf.LockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + defer tf.UnlockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + + synchronizationSecrets := synchronizationsecret.SetSynchronizationSecretRequest{ + Value: expandSynchronizationSecretKeyStringValuePair(d.Get("credential").([]interface{})), } - if servicePrincipal == nil || servicePrincipal.ID() == nil { - return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", objectId) + + if _, err := client.SetSynchronizationSecret(ctx, servicePrincipalId, synchronizationSecrets, synchronizationsecret.SetSynchronizationSecretOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Creating synchronization secret for %s", servicePrincipalId) } - synchronizationSecret := msgraph.SynchronizationSecret{ - Credentials: expandSynchronizationSecretKeyStringValuePair(d.Get("credential").([]interface{})), + // Wait for the secret to appear + if err := consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.ListSynchronizationSecrets(ctx, servicePrincipalId, synchronizationsecret.ListSynchronizationSecretsOperationOptions{RetryFunc: synchronizationRetryFunc()}) + if err != nil { + return pointer.To(false), fmt.Errorf("retrieving synchronization secret") + } + newSynchronizationSecrets := resp.Model + if newSynchronizationSecrets == nil { + return pointer.To(false), nil + } + if len(pointer.From(synchronizationSecrets.Value)) == len(pointer.From(newSynchronizationSecrets)) { + return pointer.To(true), nil + } + return pointer.To(false), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for synchronization secrets for %s", servicePrincipalId) } - _, err = client.SetSecrets(ctx, synchronizationSecret, *servicePrincipal.ID()) + d.SetId(parse.NewSynchronizationSecretID(servicePrincipalId.ServicePrincipalId).String()) + tf.Set(d, "credential", flattenSynchronizationSecretKeyStringValuePair(synchronizationSecrets.Value, nil)) + + return synchronizationSecretResourceRead(ctx, d, meta) +} + +func synchronizationSecretResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { + client := meta.(*clients.Client).Synchronization.SynchronizationSecretClient + + id, err := parse.SynchronizationSecretID(d.Id()) if err != nil { - return tf.ErrorDiagF(err, "Creating synchronization secret for service principal ID %q", *servicePrincipal.ID()) + return tf.ErrorDiagPathF(err, "id", "Parsing synchronization secret ID %q", d.Id()) } - id := parse.NewSynchronizationSecretID(*servicePrincipal.ID()) - // Wait for the secret to appear - timeout, _ := ctx.Deadline() - _, err = (&pluginsdk.StateChangeConf{ //nolint:staticcheck - Pending: []string{"Waiting"}, - Target: []string{"Done"}, - Timeout: time.Until(timeout), - MinTimeout: 1 * time.Second, - ContinuousTargetOccurence: 5, - Refresh: func() (interface{}, string, error) { - newSynchronizationSecret, _, err := client.GetSecrets(ctx, id.ServicePrincipalId) - if err != nil { - return nil, "Error", fmt.Errorf("retrieving synchronization secret") - } - if newSynchronizationSecret != nil { - if len(*synchronizationSecret.Credentials) == len(*newSynchronizationSecret.Credentials) { - return "stub", "Done", nil - } - return "stub", "Waiting", nil - } else { - return "stub", "Waiting", nil - } - }, - }).WaitForStateContext(ctx) + servicePrincipalId := stable.NewServicePrincipalID(id.ServicePrincipalId) - if err != nil { - return tf.ErrorDiagF(err, "Waiting for synchronization secret %q", id.ServicePrincipalId) + tf.LockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + defer tf.UnlockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + + synchronizationSecrets := synchronizationsecret.SetSynchronizationSecretRequest{ + Value: expandSynchronizationSecretKeyStringValuePair(d.Get("credential").([]interface{})), } - if d.IsNewResource() { - d.SetId(id.String()) + if _, err = client.SetSynchronizationSecret(ctx, servicePrincipalId, synchronizationSecrets, synchronizationsecret.SetSynchronizationSecretOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Updating synchronization secret for %s", servicePrincipalId) } - tf.Set(d, "credential", flattenSynchronizationSecretKeyStringValuePair(synchronizationSecret.Credentials, nil)) - return synchronizationSecretResourceRead(ctx, d, meta) -} + // Wait for the secret to update + if err = consistency.WaitForUpdate(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.ListSynchronizationSecrets(ctx, servicePrincipalId, synchronizationsecret.ListSynchronizationSecretsOperationOptions{RetryFunc: synchronizationRetryFunc()}) + if err != nil { + return pointer.To(false), fmt.Errorf("retrieving synchronization secret") + } + newSynchronizationSecrets := resp.Model + if newSynchronizationSecrets == nil { + return pointer.To(false), nil + } + if len(pointer.From(synchronizationSecrets.Value)) == len(pointer.From(newSynchronizationSecrets)) { + return pointer.To(true), nil + } + return pointer.To(false), nil + }); err != nil { + return tf.ErrorDiagF(err, "Waiting for synchronization secrets for %s", servicePrincipalId) + } -func synchronizationSecretResourceUpdate(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - // Update is same as create - return synchronizationSecretResourceCreate(ctx, d, meta) + tf.Set(d, "credential", flattenSynchronizationSecretKeyStringValuePair(synchronizationSecrets.Value, nil)) + + return synchronizationSecretResourceRead(ctx, d, meta) } func synchronizationSecretResourceRead(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Synchronization.SynchronizationJobClient + client := meta.(*clients.Client).Synchronization.SynchronizationSecretClient id, err := parse.SynchronizationSecretID(d.Id()) if err != nil { - return tf.ErrorDiagPathF(err, "id", "Parsing synchronization secret with ID %q", d.Id()) + return tf.ErrorDiagPathF(err, "id", "Parsing synchronization secret ID %q", d.Id()) } - secrets, status, err := client.GetSecrets(ctx, id.ServicePrincipalId) + servicePrincipalId := stable.NewServicePrincipalID(id.ServicePrincipalId) + + resp, err := client.ListSynchronizationSecrets(ctx, servicePrincipalId, synchronizationsecret.ListSynchronizationSecretsOperationOptions{RetryFunc: synchronizationRetryFunc()}) if err != nil { - if status == http.StatusNotFound { - log.Printf("[DEBUG] Synchronization secrets for service principal %q was not found - removing from state!", id.ServicePrincipalId) + if response.WasNotFound(resp.HttpResponse) { + log.Printf("[DEBUG] Synchronization secrets for %s was not found - removing from state!", servicePrincipalId) d.SetId("") return nil } - return tf.ErrorDiagF(err, "Retrieving synchronization secrets for service principal %q", id.ServicePrincipalId) + return tf.ErrorDiagF(err, "Retrieving synchronization secrets for %s", servicePrincipalId) + } + + synchronizationSecrets := resp.Model + if synchronizationSecrets == nil { + log.Printf("[DEBUG] Synchronization secrets for %s was nil - removing from state!", servicePrincipalId) + d.SetId("") + return nil } - tf.Set(d, "credential", flattenSynchronizationSecretKeyStringValuePair(secrets.Credentials, d.Get("credential").([]interface{}))) + + tf.Set(d, "credential", flattenSynchronizationSecretKeyStringValuePair(synchronizationSecrets, d.Get("credential").([]interface{}))) return nil } func synchronizationSecretResourceDelete(ctx context.Context, d *pluginsdk.ResourceData, meta interface{}) pluginsdk.Diagnostics { - client := meta.(*clients.Client).Synchronization.SynchronizationJobClient - spClient := meta.(*clients.Client).Synchronization.ServicePrincipalsClient + client := meta.(*clients.Client).Synchronization.SynchronizationSecretClient id, err := parse.SynchronizationSecretID(d.Id()) if err != nil { return tf.ErrorDiagPathF(err, "id", "Parsing synchronization secret with ID %q", d.Id()) } - tf.LockByName(servicePrincipalResourceName, id.ServicePrincipalId) - defer tf.UnlockByName(servicePrincipalResourceName, id.ServicePrincipalId) + servicePrincipalId := stable.NewServicePrincipalID(id.ServicePrincipalId) - servicePrincipal, status, err := spClient.Get(ctx, id.ServicePrincipalId, odata.Query{}) - if err != nil { - if status == http.StatusNotFound { - return tf.ErrorDiagPathF(nil, "service_principal_id", "Service principal with object ID %q was not found", id.ServicePrincipalId) - } - return tf.ErrorDiagPathF(err, "service_principal_id", "Retrieving service principal with object ID %q", id.ServicePrincipalId) - } - if servicePrincipal == nil || servicePrincipal.ID() == nil { - return tf.ErrorDiagF(errors.New("nil service principal or service principal with nil ID was returned"), "API error retrieving service principal with object ID %q", id.ServicePrincipalId) - } + tf.LockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) + defer tf.UnlockByName(servicePrincipalResourceName, servicePrincipalId.ServicePrincipalId) // We delete secrets by setting values to empty strings credentials := emptySynchronizationSecretKeyStringValuePair(d.Get("credential").([]interface{})) - synchronizationSecret := msgraph.SynchronizationSecret{ - Credentials: credentials, + synchronizationSecrets := synchronizationsecret.SetSynchronizationSecretRequest{ + Value: credentials, } - if _, err := client.SetSecrets(ctx, synchronizationSecret, id.ServicePrincipalId); err != nil { - return tf.ErrorDiagF(err, "Removing synchronization secrets for service principal with object ID %q", id.ServicePrincipalId) + if _, err := client.SetSynchronizationSecret(ctx, servicePrincipalId, synchronizationSecrets, synchronizationsecret.SetSynchronizationSecretOperationOptions{RetryFunc: synchronizationRetryFunc()}); err != nil { + return tf.ErrorDiagF(err, "Removing synchronization secrets for %s", servicePrincipalId) } // Wait for synchronization secret to be deleted - if err := helpers.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { - defer func() { client.BaseClient.DisableRetries = false }() - client.BaseClient.DisableRetries = true + if err := consistency.WaitForDeletion(ctx, func(ctx context.Context) (*bool, error) { + resp, err := client.ListSynchronizationSecrets(ctx, servicePrincipalId, synchronizationsecret.ListSynchronizationSecretsOperationOptions{RetryFunc: synchronizationRetryFunc()}) + if err != nil { + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil + } + return nil, err + } - synchronizationSecrets, _, _ := client.GetSecrets(ctx, id.ServicePrincipalId) + synchronizationSecrets := resp.Model + if synchronizationSecrets == nil { + return pointer.To(false), nil + } // Test if credentials are removed - if allCredentialsRemoved(*credentials, *synchronizationSecrets.Credentials) { + if allCredentialsRemoved(*credentials, *synchronizationSecrets) { return pointer.To(false), nil } return pointer.To(true), nil }); err != nil { - return tf.ErrorDiagF(err, "Waiting for deletion of synchronization secrets from service principal with object ID %q", id.ServicePrincipalId) + return tf.ErrorDiagF(err, "Waiting for deletion of synchronization secrets %s", servicePrincipalId) } return nil diff --git a/internal/services/synchronization/synchronization_secret_resource_test.go b/internal/services/synchronization/synchronization_secret_resource_test.go index 7a2501e3c0..8ae2042cf3 100644 --- a/internal/services/synchronization/synchronization_secret_resource_test.go +++ b/internal/services/synchronization/synchronization_secret_resource_test.go @@ -6,10 +6,12 @@ package synchronization_test import ( "context" "fmt" - "net/http" "testing" "github.com/hashicorp/go-azure-helpers/lang/pointer" + "github.com/hashicorp/go-azure-helpers/lang/response" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/common-types/stable" + "github.com/hashicorp/go-azure-sdk/microsoft-graph/serviceprincipals/stable/synchronizationsecret" "github.com/hashicorp/terraform-plugin-testing/terraform" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance" "github.com/hashicorp/terraform-provider-azuread/internal/acceptance/check" @@ -19,41 +21,13 @@ import ( type SynchronizationSecretResource struct{} -func TestAccSynchronizationSecret(t *testing.T) { - acceptance.RunTestsInSequence(t, map[string]map[string]func(t *testing.T){ - "synchronizationSecret": { - "withApplicationResource": testAccSynchronizationSecret_withApplicationResource, - "withApplicationFromTemplateResource": testAccSynchronizationSecret_withApplicationFromTemplateResource, - }, - }) -} - -func testAccSynchronizationSecret_withApplicationResource(t *testing.T) { - data := acceptance.BuildTestData(t, "azuread_synchronization_secret", "test") - r := SynchronizationSecretResource{} - - data.ResourceTest(t, r, []acceptance.TestStep{ - { - Config: r.withApplicationResource(data), - Check: acceptance.ComposeTestCheckFunc( - check.That(data.ResourceName).ExistsInAzure(r), - check.That(data.ResourceName).Key("credential.#").HasValue("2"), - check.That(data.ResourceName).Key("credential.0.key").HasValue("BaseAddress"), - check.That(data.ResourceName).Key("credential.0.value").HasValue("https://test-address.azuredatabricks.net"), - check.That(data.ResourceName).Key("credential.1.key").HasValue("SecretToken"), - check.That(data.ResourceName).Key("credential.1.value").HasValue("password-value"), - ), - }, - }) -} - -func testAccSynchronizationSecret_withApplicationFromTemplateResource(t *testing.T) { +func TestAccSynchronizationSecret_basic(t *testing.T) { data := acceptance.BuildTestData(t, "azuread_synchronization_secret", "test") r := SynchronizationSecretResource{} - data.ResourceTest(t, r, []acceptance.TestStep{ + data.ResourceTestIgnoreDangling(t, r, []acceptance.TestStep{ { - Config: r.withApplicationFromTemplateResource(data), + Config: r.basic(data), Check: acceptance.ComposeTestCheckFunc( check.That(data.ResourceName).ExistsInAzure(r), check.That(data.ResourceName).Key("credential.#").HasValue("2"), @@ -67,63 +41,31 @@ func testAccSynchronizationSecret_withApplicationFromTemplateResource(t *testing } func (r SynchronizationSecretResource) Exists(ctx context.Context, clients *clients.Client, state *terraform.InstanceState) (*bool, error) { - client := clients.ServicePrincipals.SynchronizationJobClient - client.BaseClient.DisableRetries = true - defer func() { client.BaseClient.DisableRetries = false }() + client := clients.Synchronization.SynchronizationSecretClient id, err := parse.SynchronizationSecretID(state.ID) if err != nil { - return nil, fmt.Errorf("Parsing synchronization secret from service principal %v", err) + return nil, fmt.Errorf("parsing synchronization secret from service principal %v", err) } - _, status, err := client.GetSecrets(ctx, id.ServicePrincipalId) + servicePrincipalId := stable.NewServicePrincipalID(id.ServicePrincipalId) + + resp, err := client.ListSynchronizationSecrets(ctx, servicePrincipalId, synchronizationsecret.DefaultListSynchronizationSecretsOperationOptions()) if err != nil { - if status == http.StatusNotFound { - return nil, fmt.Errorf("Synchronization secrets for service principal %q was not found ", id.ServicePrincipalId) + if response.WasNotFound(resp.HttpResponse) { + return pointer.To(false), nil } - return nil, fmt.Errorf("Retrieving synchronization secrets for service principal %q", id.ServicePrincipalId) + return nil, fmt.Errorf("retrieving synchronization secrets for %s", servicePrincipalId) } - return pointer.To(true), nil -} - -func (r SynchronizationSecretResource) withApplicationResource(data acceptance.TestData) string { - return fmt.Sprintf(` -provider "azuread" {} -data "azuread_client_config" "test" {} - -data "azuread_application_template" "test" { - display_name = "Azure Databricks SCIM Provisioning Connector" -} - -resource "azuread_application" "test" { - display_name = "acctestSynchronizationJob-%[1]d" - owners = [data.azuread_client_config.test.object_id] - template_id = data.azuread_application_template.test.template_id -} - -resource "azuread_service_principal" "test" { - application_id = azuread_application.test.application_id - owners = [data.azuread_client_config.test.object_id] - use_existing = true -} - -resource "azuread_synchronization_secret" "test" { - service_principal_id = azuread_service_principal.test.id + if resp.Model == nil { + return pointer.To(false), nil + } - credential { - key = "BaseAddress" - value = "https://test-address.azuredatabricks.net" - } - credential { - key = "SecretToken" - value = "password-value" - } -} -`, data.RandomInteger) + return pointer.To(true), nil } -func (r SynchronizationSecretResource) withApplicationFromTemplateResource(data acceptance.TestData) string { +func (r SynchronizationSecretResource) basic(data acceptance.TestData) string { return fmt.Sprintf(` provider "azuread" {}