diff --git a/client/monte_carlo_client.go b/client/monte_carlo_client.go index 8264312..39c2087 100644 --- a/client/monte_carlo_client.go +++ b/client/monte_carlo_client.go @@ -323,3 +323,35 @@ type CreateOrUpdateComparisonRule struct { } } `graphql:"createOrUpdateComparisonRule(comparisons: $comparisons, customRuleUuid: $customRuleUuid, description: $description, queryResultType: $queryResultType, scheduleConfig: $scheduleConfig, sourceConnectionId: $sourceConnectionId, sourceDwId: $sourceDwId, sourceSqlQuery: $sourceSqlQuery, targetConnectionId: $targetConnectionId, targetDwId: $targetDwId, targetSqlQuery: $targetSqlQuery)"` } + +type CreateOrUpdateServiceApiToken struct { + CreateOrUpdateServiceApiToken struct { + AccessToken struct { + Id string + Token string + } + } `graphql:"createOrUpdateServiceApiToken(comment: $comment, displayName: $displayName, expirationInDays: $expirationInDays, groups: $groups, tokenId: $tokenId)"` +} + +type TokenMetadata struct { + Id string + Comment string + CreatedBy string + CreationTime string + Email string + ExpirationTime string + FirstName string + LastName string + Groups []string + IsServiceApiToken bool +} + +type GetTokenMetadata struct { + GetTokenMetadata []TokenMetadata `graphql:"getTokenMetadata(index: $index, isServiceApiToken: $isServiceApiToken)"` +} + +type DeleteAccessToken struct { + DeleteAccessToken struct { + Success bool + } `graphql:"deleteAccessToken(tokenId: $tokenId)"` +} diff --git a/internal/authorization/service_account.go b/internal/authorization/service_account.go new file mode 100644 index 0000000..6d2f8b4 --- /dev/null +++ b/internal/authorization/service_account.go @@ -0,0 +1,182 @@ +package authorization + +import ( + "context" + "fmt" + "slices" + + "github.com/kiwicom/terraform-provider-montecarlo/client" + "github.com/kiwicom/terraform-provider-montecarlo/internal/common" + + "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/stringdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &ServiceAccountResource{} + +// This resource cannot be imported, since Token cannot be retrieved from Monte Carlo API. +// var _ resource.ResourceWithImportState = &ServiceAccountResource{} + +// To simplify provider implementations, a named function can be created with the resource implementation. +func NewServiceAccountResource() resource.Resource { + return &ServiceAccountResource{} +} + +// ServiceAccountResource defines the resource implementation. +type ServiceAccountResource struct { + client client.MonteCarloClient +} + +// ServiceAccountResourceModel describes the resource data model according to its Schema. +type ServiceAccountResourceModel struct { + Id types.String `tfsdk:"id"` + Token types.String `tfsdk:"token"` + Description types.String `tfsdk:"description"` +} + +func (r *ServiceAccountResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_service_account" +} + +func (r *ServiceAccountResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + Optional: false, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "token": schema.StringAttribute{ + Computed: true, + Optional: false, + Sensitive: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "description": schema.StringAttribute{ + Computed: true, + Optional: true, + Default: stringdefault.StaticString(""), + }, + }, + } +} + +func (r *ServiceAccountResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + client, diags := common.Configure(req) + resp.Diagnostics.Append(diags...) + r.client = client +} + +func (r *ServiceAccountResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data ServiceAccountResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + createResult := client.CreateOrUpdateServiceApiToken{} + variables := map[string]interface{}{ + "tokenId": (*string)(nil), + "comment": data.Description.ValueString(), + "displayName": (*string)(nil), + "expirationInDays": (*int)(nil), + "groups": (*[]string)(nil), + } + + if err := r.client.Mutate(ctx, &createResult, variables); err != nil { + to_print := fmt.Sprintf("MC client 'CreateOrUpdateServiceApiToken' mutation result - %s", err.Error()) + resp.Diagnostics.AddError(to_print, "") + return + } + + data.Id = types.StringValue(createResult.CreateOrUpdateServiceApiToken.AccessToken.Id) + data.Token = types.StringValue(createResult.CreateOrUpdateServiceApiToken.AccessToken.Token) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceAccountResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data ServiceAccountResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + var index int + type AccessKeyIndexEnum string + readResult := client.GetTokenMetadata{} + variables := map[string]interface{}{ + "index": (AccessKeyIndexEnum)("account"), + "isServiceApiToken": true, + } + + if err := r.client.Query(ctx, &readResult, variables); err != nil { + to_print := fmt.Sprintf("MC client 'GetTokenMetadata' query result - %s", err.Error()) + resp.Diagnostics.AddError(to_print, "") + return + } + + if index = slices.IndexFunc(readResult.GetTokenMetadata, func(token client.TokenMetadata) bool { + return token.Id == data.Id.ValueString() + }); index < 0 { + to_print := fmt.Sprintf("Token [ID: %s] not found", data.Id.ValueString()) + resp.Diagnostics.AddWarning(to_print, "") + resp.State.RemoveResource(ctx) + return + } + + data.Description = types.StringValue(readResult.GetTokenMetadata[index].Comment) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *ServiceAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data ServiceAccountResourceModel + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + updateResult := client.CreateOrUpdateServiceApiToken{} + variables := map[string]interface{}{ + "tokenId": data.Id.ValueString(), + "comment": data.Description.ValueString(), + "displayName": (*string)(nil), + "expirationInDays": (*int)(nil), + "groups": (*[]string)(nil), + } + + if err := r.client.Mutate(ctx, &updateResult, variables); err == nil { + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + } else { + to_print := fmt.Sprintf("MC client 'CreateOrUpdateServiceApiToken' mutation result - %s", err.Error()) + resp.Diagnostics.AddError(to_print, "") + } +} + +func (r *ServiceAccountResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data ServiceAccountResourceModel + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + deleteResult := client.DeleteAccessToken{} + variables := map[string]interface{}{"tokenId": data.Id.ValueString()} + + if err := r.client.Mutate(ctx, &deleteResult, variables); err != nil { + to_print := fmt.Sprintf("MC client 'DeleteAccessToken' mutation result - %s", err.Error()) + resp.Diagnostics.AddError(to_print, "") + } else if !deleteResult.DeleteAccessToken.Success { + toPrint := "MC client 'DeleteAccessToken' mutation - success = false, " + + "service account probably already doesn't exists. This resource will continue with its deletion" + resp.Diagnostics.AddWarning(toPrint, "") + } +} diff --git a/internal/authorization/service_account_test.go b/internal/authorization/service_account_test.go new file mode 100644 index 0000000..3ece557 --- /dev/null +++ b/internal/authorization/service_account_test.go @@ -0,0 +1,44 @@ +package authorization_test + +import ( + "os" + "testing" + + "github.com/kiwicom/terraform-provider-montecarlo/internal/acctest" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccServiceAccountResource(t *testing.T) { + mc_api_key_id := os.Getenv("MC_API_KEY_ID") + mc_api_key_token := os.Getenv("MC_API_KEY_TOKEN") + + resource.Test(t, resource.TestCase{ + PreCheck: func() { acctest.TestAccPreCheck(t) }, + Steps: []resource.TestStep{ + { // Create and Read testing + ProtoV6ProviderFactories: acctest.TestAccProviderFactories, + ConfigFile: config.TestNameFile("create.tf"), + ConfigVariables: config.Variables{ + "montecarlo_api_key_id": config.StringVariable(mc_api_key_id), + "montecarlo_api_key_token": config.StringVariable(mc_api_key_token), + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("montecarlo_service_account.test", "description", ""), + ), + }, + { // Update and Read testing + ProtoV6ProviderFactories: acctest.TestAccProviderFactories, + ConfigFile: config.TestNameFile("update.tf"), + ConfigVariables: config.Variables{ + "montecarlo_api_key_id": config.StringVariable(mc_api_key_id), + "montecarlo_api_key_token": config.StringVariable(mc_api_key_token), + }, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("montecarlo_service_account.test", "description", "sa-test"), + ), + }, + }, + }) +} diff --git a/internal/authorization/testdata/TestAccServiceAccountResource/create.tf b/internal/authorization/testdata/TestAccServiceAccountResource/create.tf new file mode 100644 index 0000000..cba663f --- /dev/null +++ b/internal/authorization/testdata/TestAccServiceAccountResource/create.tf @@ -0,0 +1,16 @@ +variable "montecarlo_api_key_id" { + type = string +} + +variable "montecarlo_api_key_token" { + type = string +} + +provider "montecarlo" { + account_service_key = { + id = var.montecarlo_api_key_id # (secret) + token = var.montecarlo_api_key_token # (secret) + } +} + +resource "montecarlo_service_account" "test" {} diff --git a/internal/authorization/testdata/TestAccServiceAccountResource/update.tf b/internal/authorization/testdata/TestAccServiceAccountResource/update.tf new file mode 100644 index 0000000..7222960 --- /dev/null +++ b/internal/authorization/testdata/TestAccServiceAccountResource/update.tf @@ -0,0 +1,18 @@ +variable "montecarlo_api_key_id" { + type = string +} + +variable "montecarlo_api_key_token" { + type = string +} + +provider "montecarlo" { + account_service_key = { + id = var.montecarlo_api_key_id # (secret) + token = var.montecarlo_api_key_token # (secret) + } +} + +resource "montecarlo_service_account" "test" { + description = "sa-test" +} diff --git a/internal/domain.go b/internal/domain.go index 993aad0..770f855 100644 --- a/internal/domain.go +++ b/internal/domain.go @@ -216,7 +216,6 @@ func (r *DomainResource) Delete(ctx context.Context, req resource.DeleteRequest, if err := r.client.Mutate(ctx, &deleteResult, variables); err != nil { toPrint := fmt.Sprintf("MC client 'DeleteDomain' mutation result - %s", err.Error()) resp.Diagnostics.AddError(toPrint, "") - return } else if deleteResult.DeleteDomain.Deleted != 1 { toPrint := fmt.Sprintf("MC client 'DeleteDomain' mutation - deleted = %d, "+ "expected result is 1 - more domains might have been deleted. This resource "+ diff --git a/internal/provider.go b/internal/provider.go index bb2561a..2741703 100644 --- a/internal/provider.go +++ b/internal/provider.go @@ -99,6 +99,7 @@ func (p *Provider) Resources(ctx context.Context) []func() resource.Resource { authorization.NewIamGroupResource, authorization.NewIamMemberResource, //monitor.NewComparisonMonitorResource, + authorization.NewServiceAccountResource, } } diff --git a/internal/warehouse/bigquery_warehouse.go b/internal/warehouse/bigquery_warehouse.go index b45acdb..5a533cf 100644 --- a/internal/warehouse/bigquery_warehouse.go +++ b/internal/warehouse/bigquery_warehouse.go @@ -290,7 +290,6 @@ func (r *BigQueryWarehouseResource) Delete(ctx context.Context, req resource.Del if err := r.client.Mutate(ctx, &removeResult, variables); err != nil { toPrint := fmt.Sprintf("MC client 'RemoveConnection' mutation result - %s", err.Error()) resp.Diagnostics.AddError(toPrint, "") - return } else if !removeResult.RemoveConnection.Success { toPrint := "MC client 'RemoveConnection' mutation - success = false, " + "connection probably already doesn't exists. This resource will continue with its deletion" diff --git a/internal/warehouse/transactional_warehouse.go b/internal/warehouse/transactional_warehouse.go index 9a81998..27b96f4 100644 --- a/internal/warehouse/transactional_warehouse.go +++ b/internal/warehouse/transactional_warehouse.go @@ -318,7 +318,6 @@ func (r *TransactionalWarehouseResource) Delete(ctx context.Context, req resourc if err := r.client.Mutate(ctx, &removeResult, variables); err != nil { toPrint := fmt.Sprintf("MC client 'RemoveConnection' mutation result - %s", err.Error()) resp.Diagnostics.AddError(toPrint, "") - return } else if !removeResult.RemoveConnection.Success { toPrint := "MC client 'RemoveConnection' mutation - success = false, " + "connection probably already doesn't exists. This resource will continue with its deletion"