From 1d3388acd6ca867234ea3f13ce1abf41208830d7 Mon Sep 17 00:00:00 2001 From: Matthias Theuermann <73223147+mati007thm@users.noreply.github.com> Date: Tue, 10 Dec 2024 20:59:39 +0100 Subject: [PATCH 1/6] =?UTF-8?q?=E2=9C=A8=20new=20resource=20mondoo=5Finteg?= =?UTF-8?q?ration=5Femail=20(#169)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: type assertion * fix: type assertion * fix: requested changes * fix: expanded test cases * fix: changed resource name for tests --------- Signed-off-by: Matthias Theuermann --- docs/resources/integration_email.md | 73 +++ .../mondoo_integration_email/main.tf | 8 + .../mondoo_integration_email/resource.tf | 26 + internal/provider/gql.go | 14 + .../provider/integration_email_resource.go | 450 ++++++++++++++++++ .../integration_email_resource_test.go | 102 ++++ internal/provider/provider.go | 1 + 7 files changed, 674 insertions(+) create mode 100644 docs/resources/integration_email.md create mode 100644 examples/resources/mondoo_integration_email/main.tf create mode 100644 examples/resources/mondoo_integration_email/resource.tf create mode 100644 internal/provider/integration_email_resource.go create mode 100644 internal/provider/integration_email_resource_test.go diff --git a/docs/resources/integration_email.md b/docs/resources/integration_email.md new file mode 100644 index 0000000..f6649a2 --- /dev/null +++ b/docs/resources/integration_email.md @@ -0,0 +1,73 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "mondoo_integration_email Resource - terraform-provider-mondoo" +subcategory: "" +description: |- + Send an email to your ticket system, or any recipient. +--- + +# mondoo_integration_email (Resource) + +Send an email to your ticket system, or any recipient. + +## Example Usage + +```terraform +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the Email integration +resource "mondoo_integration_email" "email_integration" { + name = "My Email Integration" + + recipients = [ + { + name = "John Doe" + email = "john@example.com" + is_default = true + reference_url = "https://example.com" + }, + { + name = "Alice Doe" + email = "alice@example.com" + is_default = false + reference_url = "https://example.com" + } + ] + + auto_create = true + auto_close = true +} +``` + + +## Schema + +### Required + +- `name` (String) Name of the integration. +- `recipients` (Attributes List) List of email recipients. (see [below for nested schema](#nestedatt--recipients)) + +### Optional + +- `auto_close` (Boolean) Auto close tickets (defaults to false). +- `auto_create` (Boolean) Auto create tickets (defaults to false). +- `space_id` (String) Mondoo Space Identifier. If it is not provided, the provider space is used. + +### Read-Only + +- `mrn` (String) Integration identifier + + +### Nested Schema for `recipients` + +Required: + +- `email` (String) Recipient email address. +- `name` (String) Recipient name. + +Optional: + +- `is_default` (Boolean) Mark this recipient as default. This needs to be set if auto_create is enabled. +- `reference_url` (String) Reference URL for the recipient. diff --git a/examples/resources/mondoo_integration_email/main.tf b/examples/resources/mondoo_integration_email/main.tf new file mode 100644 index 0000000..24d24a1 --- /dev/null +++ b/examples/resources/mondoo_integration_email/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + mondoo = { + source = "mondoohq/mondoo" + version = ">= 0.19" + } + } +} \ No newline at end of file diff --git a/examples/resources/mondoo_integration_email/resource.tf b/examples/resources/mondoo_integration_email/resource.tf new file mode 100644 index 0000000..17b0727 --- /dev/null +++ b/examples/resources/mondoo_integration_email/resource.tf @@ -0,0 +1,26 @@ +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the Email integration +resource "mondoo_integration_email" "email_integration" { + name = "My Email Integration" + + recipients = [ + { + name = "John Doe" + email = "john@example.com" + is_default = true + reference_url = "https://example.com" + }, + { + name = "Alice Doe" + email = "alice@example.com" + is_default = false + reference_url = "https://example.com" + } + ] + + auto_create = true + auto_close = true +} diff --git a/internal/provider/gql.go b/internal/provider/gql.go index 2553ab7..3f5f840 100644 --- a/internal/provider/gql.go +++ b/internal/provider/gql.go @@ -617,6 +617,19 @@ type ShodanConfigurationOptions struct { Targets []string } +type EmailConfigurationOptions struct { + Recipients []EmailRecipient + AutoCreateTickets bool + AutoCloseTickets bool +} + +type EmailRecipient struct { + Name string + Email string + IsDefault bool + ReferenceURL string +} + type ClientIntegrationConfigurationOptions struct { AzureConfigurationOptions AzureConfigurationOptions `graphql:"... on AzureConfigurationOptions"` HostConfigurationOptions HostConfigurationOptions `graphql:"... on HostConfigurationOptions"` @@ -626,6 +639,7 @@ type ClientIntegrationConfigurationOptions struct { GithubConfigurationOptions GithubConfigurationOptions `graphql:"... on GithubConfigurationOptions"` HostedAwsConfigurationOptions HostedAwsConfigurationOptions `graphql:"... on HostedAwsConfigurationOptions"` ShodanConfigurationOptions ShodanConfigurationOptions `graphql:"... on ShodanConfigurationOptions"` + EmailConfigurationOptions EmailConfigurationOptions `graphql:"... on EmailConfigurationOptions"` GitlabConfigurationOptions GitlabConfigurationOptions `graphql:"... on GitlabConfigurationOptions"` // Add other configuration options here } diff --git a/internal/provider/integration_email_resource.go b/internal/provider/integration_email_resource.go new file mode 100644 index 0000000..2e7c50b --- /dev/null +++ b/internal/provider/integration_email_resource.go @@ -0,0 +1,450 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "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-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + mondoov1 "go.mondoo.com/mondoo-go" +) + +var _ resource.Resource = (*integrationEmailResource)(nil) + +func NewIntegrationEmailResource() resource.Resource { + return &integrationEmailResource{} +} + +type integrationEmailResource struct { + client *ExtendedGqlClient +} + +type integrationEmailResourceModel struct { + // scope + SpaceID types.String `tfsdk:"space_id"` + + // integration details + Mrn types.String `tfsdk:"mrn"` + Name types.String `tfsdk:"name"` + Recipients *[]integrationEmailRecipientInput `tfsdk:"recipients"` + AutoCreateTickets types.Bool `tfsdk:"auto_create"` + AutoCloseTickets types.Bool `tfsdk:"auto_close"` +} + +type integrationEmailRecipientInput struct { + Name types.String `tfsdk:"name"` + Email types.String `tfsdk:"email"` + IsDefault types.Bool `tfsdk:"is_default"` + ReferenceURL types.String `tfsdk:"reference_url"` +} + +func (m integrationEmailResourceModel) GetConfigurationOptions() *mondoov1.EmailConfigurationOptionsInput { + opts := &mondoov1.EmailConfigurationOptionsInput{ + Recipients: convertRecipients(m.Recipients), + AutoCreateTickets: mondoov1.NewBooleanPtr(mondoov1.Boolean(m.AutoCreateTickets.ValueBool())), + AutoCloseTickets: mondoov1.NewBooleanPtr(mondoov1.Boolean(m.AutoCloseTickets.ValueBool())), + } + + return opts +} + +func (r *integrationEmailResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_integration_email" +} + +type defaultRecipientValidator struct{} + +func (d defaultRecipientValidator) ValidateList(ctx context.Context, req validator.ListRequest, resp *validator.ListResponse) { + // Return early if value is null or unknown + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + // Convert ListValue to a slice of ObjectValues + var recipients []basetypes.ObjectValue + diags := req.ConfigValue.ElementsAs(ctx, &recipients, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + defaultCount := 0 + + // Iterate over recipients to count default ones + for _, recipient := range recipients { + // Access the attributes of the recipient + attrs := recipient.Attributes() + + // Retrieve the "is_default" attribute + isDefaultAttr, exists := attrs["is_default"] + if !exists { + resp.Diagnostics.AddError( + "Missing Attribute", + "Recipient object is missing the 'is_default' attribute.", + ) + return + } + + // Check if the value is true + isDefault, ok := isDefaultAttr.(types.Bool) + if !ok { + resp.Diagnostics.AddError( + "Invalid Attribute Type", + "The 'is_default' attribute must be a boolean.", + ) + return + } + + if isDefault.ValueBool() { + defaultCount++ + } + } + + // Validate that only one recipient is marked as default + if defaultCount > 1 { + resp.Diagnostics.AddError( + "Too Many Default Recipients", + "Only one recipient can be marked as default.", + ) + } +} + +func (d defaultRecipientValidator) Description(ctx context.Context) string { + return "Ensures that only one recipient is marked as default." +} + +func (d defaultRecipientValidator) MarkdownDescription(ctx context.Context) string { + return "Ensures that only one recipient is marked as `default`." +} + +func NewDefaultRecipientValidator() validator.List { + return defaultRecipientValidator{} +} + +type AutoCreateValidator struct{} + +func (v AutoCreateValidator) ValidateBool(ctx context.Context, req validator.BoolRequest, resp *validator.BoolResponse) { + if req.ConfigValue.IsNull() || req.ConfigValue.IsUnknown() { + return + } + + autoCreate := req.ConfigValue.ValueBool() + if !autoCreate { + return + } + + // Retrieve the recipients list from configuration + var recipientsAttr basetypes.ListValue + recipientsDiags := req.Config.GetAttribute(ctx, path.Root("recipients"), &recipientsAttr) + if recipientsDiags.HasError() { + resp.Diagnostics.Append(recipientsDiags...) + return + } + + var recipients []basetypes.ObjectValue + diags := recipientsAttr.ElementsAs(ctx, &recipients, false) + if diags.HasError() { + resp.Diagnostics.Append(diags...) + return + } + + defaultFound := false + for _, recipient := range recipients { + attrs := recipient.Attributes() + + isDefaultAttr, exists := attrs["is_default"] + if exists { + isDefault, ok := isDefaultAttr.(types.Bool) + if !ok { + resp.Diagnostics.AddError( + "Invalid Attribute Type", + "The 'is_default' attribute must be a boolean.", + ) + return + } + if isDefault.ValueBool() { + defaultFound = true + break + } + } + } + + if !defaultFound { + resp.Diagnostics.AddError( + "Missing Default Recipient", + "At least one recipient must be marked as default when auto-create is enabled.", + ) + } +} + +func (v AutoCreateValidator) Description(ctx context.Context) string { + return "Ensures that at least one recipient is marked as default when auto-create is enabled." +} + +func (v AutoCreateValidator) MarkdownDescription(ctx context.Context) string { + return "Ensures that at least one recipient is marked as `default` when auto-create is enabled." +} + +func NewAutoCreateValidator() validator.Bool { + return AutoCreateValidator{} +} + +func (r *integrationEmailResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "Send an email to your ticket system, or any recipient.", + Attributes: map[string]schema.Attribute{ + "space_id": schema.StringAttribute{ + MarkdownDescription: "Mondoo Space Identifier. If it is not provided, the provider space is used.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "mrn": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Integration identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the integration.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(250), + }, + }, + "recipients": schema.ListNestedAttribute{ + MarkdownDescription: "List of email recipients.", + Required: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "name": schema.StringAttribute{ + MarkdownDescription: "Recipient name.", + Required: true, + }, + "email": schema.StringAttribute{ + MarkdownDescription: "Recipient email address.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), + "must be a valid email", + ), + }, + }, + "is_default": schema.BoolAttribute{ + MarkdownDescription: "Mark this recipient as default. This needs to be set if auto_create is enabled.", + Optional: true, + }, + "reference_url": schema.StringAttribute{ + MarkdownDescription: "Reference URL for the recipient.", + Optional: true, + }, + }, + }, + Validators: []validator.List{ + NewDefaultRecipientValidator(), + }, + }, + "auto_create": schema.BoolAttribute{ + MarkdownDescription: "Auto create tickets (defaults to false).", + Optional: true, + Validators: []validator.Bool{ + NewAutoCreateValidator(), + }, + }, + "auto_close": schema.BoolAttribute{ + MarkdownDescription: "Auto close tickets (defaults to false).", + Optional: true, + }, + }, + } +} + +func (r *integrationEmailResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*ExtendedGqlClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func convertRecipients(recipients *[]integrationEmailRecipientInput) []mondoov1.EmailRecipientInput { + if recipients == nil { + return nil + } + var result []mondoov1.EmailRecipientInput + for _, r := range *recipients { + result = append(result, mondoov1.EmailRecipientInput{ + Name: mondoov1.String(r.Name.ValueString()), + Email: mondoov1.String(r.Email.ValueString()), + IsDefault: mondoov1.Boolean(r.IsDefault.ValueBool()), + ReferenceURL: mondoov1.NewStringPtr(mondoov1.String(r.ReferenceURL.ValueString())), + }) + } + return result +} + +func (r *integrationEmailResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data integrationEmailResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + // Compute and validate the space + space, err := r.client.ComputeSpace(data.SpaceID) + if err != nil { + resp.Diagnostics.AddError("Invalid Configuration", err.Error()) + return + } + ctx = tflog.SetField(ctx, "space_mrn", space.MRN()) + + // Do GraphQL request to API to create the resource. + tflog.Debug(ctx, "Creating integration") + integration, err := r.client.CreateIntegration(ctx, + space.MRN(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeTicketSystemEmail, + mondoov1.ClientIntegrationConfigurationInput{ + EmailConfigurationOptions: data.GetConfigurationOptions(), + }) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to create Email integration, got error: %s", err), + ) + return + } + + // Save space mrn into the Terraform state. + data.Mrn = types.StringValue(string(integration.Mrn)) + data.Name = types.StringValue(string(integration.Name)) + data.SpaceID = types.StringValue(space.ID()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationEmailResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data integrationEmailResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationEmailResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data integrationEmailResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + opts := mondoov1.ClientIntegrationConfigurationInput{ + EmailConfigurationOptions: data.GetConfigurationOptions(), + } + + _, err := r.client.UpdateIntegration(ctx, + data.Mrn.ValueString(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeTicketSystemEmail, + opts, + ) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to update Email integration, got error: %s", err), + ) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationEmailResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data integrationEmailResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + _, err := r.client.DeleteIntegration(ctx, data.Mrn.ValueString()) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to delete Email integration, got error: %s", err), + ) + return + } +} + +func (r *integrationEmailResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + integration, ok := r.client.ImportIntegration(ctx, req, resp) + if !ok { + return + } + + var recipients []integrationEmailRecipientInput + for _, recipient := range integration.ConfigurationOptions.EmailConfigurationOptions.Recipients { + recipients = append(recipients, integrationEmailRecipientInput{ + Name: types.StringValue(recipient.Name), + Email: types.StringValue(recipient.Email), + IsDefault: types.BoolValue(recipient.IsDefault), + ReferenceURL: types.StringValue(recipient.ReferenceURL), + }) + } + + model := integrationEmailResourceModel{ + Mrn: types.StringValue(integration.Mrn), + Name: types.StringValue(integration.Name), + SpaceID: types.StringValue(integration.SpaceID()), + AutoCreateTickets: types.BoolValue(integration.ConfigurationOptions.EmailConfigurationOptions.AutoCreateTickets), + AutoCloseTickets: types.BoolValue(integration.ConfigurationOptions.EmailConfigurationOptions.AutoCloseTickets), + Recipients: &recipients, + } + + resp.State.Set(ctx, &model) +} diff --git a/internal/provider/integration_email_resource_test.go b/internal/provider/integration_email_resource_test.go new file mode 100644 index 0000000..b2defe2 --- /dev/null +++ b/internal/provider/integration_email_resource_test.go @@ -0,0 +1,102 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccEmailIntegrationResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "one", []map[string]interface{}{ + {"name": "John Doe", "email": "john@example.com", "is_default": true, "reference_url": "https://example.com"}, + {"name": "Alice Doe", "email": "alice@example.com", "is_default": false, "reference_url": "https://example.com"}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "recipients.0.name", "John Doe"), + ), + }, + { + Config: testAccEmailIntegrationResourceWithSpaceInProviderConfig(accSpace.ID(), "two", true, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "auto_create", "true"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "auto_close", "true"), + ), + }, + // Update and Read testing + { + Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "three", []map[string]interface{}{ + {"name": "John Doe", "email": "john.doe@example.com", "is_default": true, "reference_url": "https://newurl.com"}, + {"name": "Alice Doe", "email": "alice.doe@example.com", "is_default": false, "reference_url": "https://newurl.com"}, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "three"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "recipients.0.reference_url", "https://newurl.com"), + ), + }, + { + Config: testAccEmailIntegrationResourceWithSpaceInProviderConfig(accSpace.ID(), "four", false, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "four"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "auto_create", "false"), + resource.TestCheckResourceAttr("mondoo_integration_email.test", "auto_close", "false"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccEmailIntegrationResourceConfig(spaceID, intName string, recipients []map[string]interface{}) string { + return fmt.Sprintf(` +resource "mondoo_integration_email" "test" { + space_id = %[1]q + name = %[2]q + recipients = %[3]q + auto_create = true + auto_close = true +} +`, spaceID, intName, recipients) +} + +func testAccEmailIntegrationResourceWithSpaceInProviderConfig(spaceID, intName string, autoCreate, autoClose bool) string { + return fmt.Sprintf(` +provider "mondoo" { + space = %[1]q +} +resource "mondoo_integration_email" "email_integration" { + name = %[2]q + recipients = [ + { + name = "John Doe" + email = "john@example.com" + is_default = true + reference_url = "https://example.com" + }, + { + name = "Alice Doe" + email = "alice@example.com" + is_default = false + reference_url = "https://example.com" + } + ] + auto_create = %[3]t + auto_close = %[4]t +} +`, spaceID, intName, autoCreate, autoClose) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4779b2f..4995328 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -204,6 +204,7 @@ func (p *MondooProvider) Resources(ctx context.Context) []func() resource.Resour NewIntegrationShodanResource, NewFrameworkAssignmentResource, NewCustomFrameworkResource, + NewIntegrationEmailResource, NewIntegrationGitlabResource, } } From feb4fe35892830a3d6dc4a93145193a040b76497 Mon Sep 17 00:00:00 2001 From: Matthias Theuermann <73223147+mati007thm@users.noreply.github.com> Date: Tue, 10 Dec 2024 21:26:40 +0100 Subject: [PATCH 2/6] =?UTF-8?q?=E2=9C=85=20=20added=20test=20cases=20for?= =?UTF-8?q?=20gitlab=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Matthias Theuermann --- .../integration_gitlab_resource_test.go | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 internal/provider/integration_gitlab_resource_test.go diff --git a/internal/provider/integration_gitlab_resource_test.go b/internal/provider/integration_gitlab_resource_test.go new file mode 100644 index 0000000..f1c4e0b --- /dev/null +++ b/internal/provider/integration_gitlab_resource_test.go @@ -0,0 +1,105 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccGitLabResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccGitLabResourceConfig(accSpace.ID(), "one", true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.0.groups", "true"), + ), + }, + { + Config: testAccGitLabResourceWithSpaceInProviderConfig(accSpace.ID(), "two", "abctoken12345"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.0.token", "abctoken12345"), + ), + }, + // ImportState testing + // @afiune this doesn't work since most of our resources doesn't have the `id` attribute + // if we add it, instead of the `mrn` or as a copy, this import test will work + // { + // ResourceName: "mondoo_integration_gitlab.test", + // ImportState: true, + // ImportStateVerify: true, + // }, + // Update and Read testing + { + Config: testAccGitLabResourceConfig(accSpace.ID(), "one", true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.0.groups", "true"), + ), + }, + { + Config: testAccGitLabResourceWithSpaceInProviderConfig(accSpace.ID(), "two", "abctoken12345"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.0.token", "abctoken12345"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccGitLabResourceConfig(spaceID, intName string, discoveryGroup bool) string { + return fmt.Sprintf(` +resource "mondoo_integration_gitlab" "test" { + space_id = %[1]q + name = %[2]q + base_url = "https://my-self-hosted-gitlab.com" + group = "my-group" + discovery = { + groups = %[3]t + projects = true + terraform = true + k8s_manifests = true + } + credentials = { + token = "abcd1234567890" + } +} +`, spaceID, intName, discoveryGroup) +} + +func testAccGitLabResourceWithSpaceInProviderConfig(spaceID, intName, token string) string { + return fmt.Sprintf(` +provider "mondoo" { + space = %[1]q +} +resource "mondoo_integration_gitlab" "test" { + name = %[2]q + base_url = "https://my-self-hosted-gitlab.com" + group = "my-group" + discovery = { + groups = true + projects = true + terraform = true + k8s_manifests = true + } + credentials = { + token = %[3]q + } +} +`, spaceID, intName, token) +} From 74bd074c12a044135a3559d05cda963a7a37c5de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:38:37 -0800 Subject: [PATCH 3/6] Bump go.mondoo.com/cnquery/v11 in the gomodupdates group (#179) Bumps the gomodupdates group with 1 update: [go.mondoo.com/cnquery/v11](https://github.com/mondoohq/cnquery). Updates `go.mondoo.com/cnquery/v11` from 11.33.0 to 11.33.1 - [Release notes](https://github.com/mondoohq/cnquery/releases) - [Changelog](https://github.com/mondoohq/cnquery/blob/main/.goreleaser.yml) - [Commits](https://github.com/mondoohq/cnquery/compare/v11.33.0...v11.33.1) --- updated-dependencies: - dependency-name: go.mondoo.com/cnquery/v11 dependency-type: direct:production update-type: version-update:semver-patch dependency-group: gomodupdates ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3c0369e..d5902c1 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.11.0 github.com/stretchr/testify v1.10.0 - go.mondoo.com/cnquery/v11 v11.33.0 + go.mondoo.com/cnquery/v11 v11.33.1 go.mondoo.com/mondoo-go v0.0.0-20241118222255-5299c9adc97c gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index d0dc833..513e48c 100644 --- a/go.sum +++ b/go.sum @@ -605,8 +605,8 @@ go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= -go.mondoo.com/cnquery/v11 v11.33.0 h1:lXLEPwt+7D3GW2hKMNmHlQlD6YhEd3izGnmHIo2a3Kg= -go.mondoo.com/cnquery/v11 v11.33.0/go.mod h1:ynuOojMFVuwUAq7nC0Dk6Ut/2MS9T/R+hHmWQdP491Q= +go.mondoo.com/cnquery/v11 v11.33.1 h1:ppAKcz3PG80AXKclDYOR3w89B7HgszDw3OSStJj4CcI= +go.mondoo.com/cnquery/v11 v11.33.1/go.mod h1:ynuOojMFVuwUAq7nC0Dk6Ut/2MS9T/R+hHmWQdP491Q= go.mondoo.com/mondoo-go v0.0.0-20241118222255-5299c9adc97c h1:0u12icLFjeTLzNQHjPs8Mw65VG1Wl8LxHoGRihwaSmg= go.mondoo.com/mondoo-go v0.0.0-20241118222255-5299c9adc97c/go.mod h1:VTTbqYTjin1hKSnwKHVYeOTEyJrAZarNlf1I8M2rlpM= go.mondoo.com/ranger-rpc v0.6.4 h1:q01kjESvF2HSnbFO+TjpUQSiI2IM8JWGJLH3u0vNxZA= From 0496f60dc3ca7b6ed89e15954816de0e9907d957 Mon Sep 17 00:00:00 2001 From: Salim Afiune Maya Date: Tue, 10 Dec 2024 15:57:00 -0800 Subject: [PATCH 4/6] =?UTF-8?q?=F0=9F=A7=B9=20fix=20acceptance=20tests=20(?= =?UTF-8?q?#180)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Salim Afiune Maya --- .../integration_email_resource_test.go | 18 ++++----- .../integration_gitlab_resource_test.go | 38 +++++++++---------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/internal/provider/integration_email_resource_test.go b/internal/provider/integration_email_resource_test.go index b2defe2..d9ca9bf 100644 --- a/internal/provider/integration_email_resource_test.go +++ b/internal/provider/integration_email_resource_test.go @@ -17,10 +17,10 @@ func TestAccEmailIntegrationResource(t *testing.T) { Steps: []resource.TestStep{ // Create and Read testing { - Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "one", []map[string]interface{}{ + Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "one", `[ {"name": "John Doe", "email": "john@example.com", "is_default": true, "reference_url": "https://example.com"}, - {"name": "Alice Doe", "email": "alice@example.com", "is_default": false, "reference_url": "https://example.com"}, - }), + {"name": "Alice Doe", "email": "alice@example.com", "is_default": false, "reference_url": "https://example.com"} + ]`), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "one"), resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), @@ -38,10 +38,10 @@ func TestAccEmailIntegrationResource(t *testing.T) { }, // Update and Read testing { - Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "three", []map[string]interface{}{ + Config: testAccEmailIntegrationResourceConfig(accSpace.ID(), "three", `[ {"name": "John Doe", "email": "john.doe@example.com", "is_default": true, "reference_url": "https://newurl.com"}, - {"name": "Alice Doe", "email": "alice.doe@example.com", "is_default": false, "reference_url": "https://newurl.com"}, - }), + {"name": "Alice Doe", "email": "alice.doe@example.com", "is_default": false, "reference_url": "https://newurl.com"} + ]`), Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_email.test", "name", "three"), resource.TestCheckResourceAttr("mondoo_integration_email.test", "space_id", accSpace.ID()), @@ -62,12 +62,12 @@ func TestAccEmailIntegrationResource(t *testing.T) { }) } -func testAccEmailIntegrationResourceConfig(spaceID, intName string, recipients []map[string]interface{}) string { +func testAccEmailIntegrationResourceConfig(spaceID, intName, recipients string) string { return fmt.Sprintf(` resource "mondoo_integration_email" "test" { space_id = %[1]q name = %[2]q - recipients = %[3]q + recipients = %[3]s auto_create = true auto_close = true } @@ -79,7 +79,7 @@ func testAccEmailIntegrationResourceWithSpaceInProviderConfig(spaceID, intName s provider "mondoo" { space = %[1]q } -resource "mondoo_integration_email" "email_integration" { +resource "mondoo_integration_email" "test" { name = %[2]q recipients = [ { diff --git a/internal/provider/integration_gitlab_resource_test.go b/internal/provider/integration_gitlab_resource_test.go index f1c4e0b..04ddee1 100644 --- a/internal/provider/integration_gitlab_resource_test.go +++ b/internal/provider/integration_gitlab_resource_test.go @@ -21,7 +21,7 @@ func TestAccGitLabResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "one"), resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), - resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.0.groups", "true"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.groups", "true"), ), }, { @@ -29,7 +29,7 @@ func TestAccGitLabResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "two"), resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), - resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.0.token", "abctoken12345"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.token", "abctoken12345"), ), }, // ImportState testing @@ -46,7 +46,7 @@ func TestAccGitLabResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "one"), resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), - resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.0.groups", "true"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "discovery.groups", "true"), ), }, { @@ -54,7 +54,7 @@ func TestAccGitLabResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "name", "two"), resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "space_id", accSpace.ID()), - resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.0.token", "abctoken12345"), + resource.TestCheckResourceAttr("mondoo_integration_gitlab.test", "credentials.token", "abctoken12345"), ), }, // Delete testing automatically occurs in TestCase @@ -66,17 +66,17 @@ func testAccGitLabResourceConfig(spaceID, intName string, discoveryGroup bool) s return fmt.Sprintf(` resource "mondoo_integration_gitlab" "test" { space_id = %[1]q - name = %[2]q + name = %[2]q base_url = "https://my-self-hosted-gitlab.com" - group = "my-group" + group = "my-group" discovery = { - groups = %[3]t - projects = true - terraform = true - k8s_manifests = true + groups = %[3]t + projects = true + terraform = true + k8s_manifests = true } credentials = { - token = "abcd1234567890" + token = "abcd1234567890" } } `, spaceID, intName, discoveryGroup) @@ -85,20 +85,20 @@ resource "mondoo_integration_gitlab" "test" { func testAccGitLabResourceWithSpaceInProviderConfig(spaceID, intName, token string) string { return fmt.Sprintf(` provider "mondoo" { - space = %[1]q + space = %[1]q } resource "mondoo_integration_gitlab" "test" { - name = %[2]q + name = %[2]q base_url = "https://my-self-hosted-gitlab.com" - group = "my-group" + group = "my-group" discovery = { - groups = true - projects = true - terraform = true - k8s_manifests = true + groups = true + projects = true + terraform = true + k8s_manifests = true } credentials = { - token = %[3]q + token = %[3]q } } `, spaceID, intName, token) From 75b5a1cf04e707f50af4d7ece64ca5e01c1b204a Mon Sep 17 00:00:00 2001 From: Matthias Theuermann <73223147+mati007thm@users.noreply.github.com> Date: Fri, 13 Dec 2024 05:59:26 +0100 Subject: [PATCH 5/6] Jira integration (#168) fixes https://github.com/mondoohq/terraform-provider-mondoo/issues/160 --------- Signed-off-by: Matthias Theuermann --- .github/actions/spelling/expect.txt | 1 + docs/resources/integration_jira.md | 68 ++++ .../resources/mondoo_integration_jira/main.tf | 8 + .../mondoo_integration_jira/resource.tf | 24 ++ internal/provider/gql.go | 9 + .../provider/integration_jira_resource.go | 295 ++++++++++++++++++ .../integration_jira_resource_test.go | 103 ++++++ internal/provider/provider.go | 1 + 8 files changed, 509 insertions(+) create mode 100644 docs/resources/integration_jira.md create mode 100644 examples/resources/mondoo_integration_jira/main.tf create mode 100644 examples/resources/mondoo_integration_jira/resource.tf create mode 100644 internal/provider/integration_jira_resource.go create mode 100644 internal/provider/integration_jira_resource_test.go diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index e182c6c..7c88240 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -15,6 +15,7 @@ Hmj JFB JFUz Jhb +jira KBp ljq LQV diff --git a/docs/resources/integration_jira.md b/docs/resources/integration_jira.md new file mode 100644 index 0000000..a6f0f17 --- /dev/null +++ b/docs/resources/integration_jira.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "mondoo_integration_jira Resource - terraform-provider-mondoo" +subcategory: "" +description: |- + Integrate the Ticketing System Jira with Mondoo to automatically create and close issues based on Mondoo findings. +--- + +# mondoo_integration_jira (Resource) + +Integrate the Ticketing System Jira with Mondoo to automatically create and close issues based on Mondoo findings. + +## Example Usage + +```terraform +variable "jira_token" { + description = "The Jira API Token" + type = string + sensitive = true +} + +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the Jira integration +resource "mondoo_integration_jira" "jira_integration" { + name = "My Jira Integration" + host = "https://your-instance.atlassian.net" + email = "jira.owner@email.com" + # default_project = "MONDOO" + + auto_create = true + auto_close = true + + credentials = { + token = var.jira_token + } +} +``` + + +## Schema + +### Required + +- `credentials` (Attributes) (see [below for nested schema](#nestedatt--credentials)) +- `email` (String) Jira user email. +- `host` (String) Jira host URL. +- `name` (String) Name of the integration. + +### Optional + +- `auto_close` (Boolean) Automatically close Jira issues for resolved Mondoo findings +- `auto_create` (Boolean) Automatically create Jira issues for Mondoo findings. +- `default_project` (String) Default Jira project (is represented by the project key e.g. `MONDOO`). +- `space_id` (String) Mondoo Space Identifier. If it is not provided, the provider space is used. + +### Read-Only + +- `mrn` (String) Integration identifier. + + +### Nested Schema for `credentials` + +Required: + +- `token` (String, Sensitive) Jira API token. diff --git a/examples/resources/mondoo_integration_jira/main.tf b/examples/resources/mondoo_integration_jira/main.tf new file mode 100644 index 0000000..24d24a1 --- /dev/null +++ b/examples/resources/mondoo_integration_jira/main.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + mondoo = { + source = "mondoohq/mondoo" + version = ">= 0.19" + } + } +} \ No newline at end of file diff --git a/examples/resources/mondoo_integration_jira/resource.tf b/examples/resources/mondoo_integration_jira/resource.tf new file mode 100644 index 0000000..de60f4e --- /dev/null +++ b/examples/resources/mondoo_integration_jira/resource.tf @@ -0,0 +1,24 @@ +variable "jira_token" { + description = "The Jira API Token" + type = string + sensitive = true +} + +provider "mondoo" { + space = "hungry-poet-123456" +} + +# Setup the Jira integration +resource "mondoo_integration_jira" "jira_integration" { + name = "My Jira Integration" + host = "https://your-instance.atlassian.net" + email = "jira.owner@email.com" + # default_project = "MONDOO" + + auto_create = true + auto_close = true + + credentials = { + token = var.jira_token + } +} diff --git a/internal/provider/gql.go b/internal/provider/gql.go index 3f5f840..4454e72 100644 --- a/internal/provider/gql.go +++ b/internal/provider/gql.go @@ -617,6 +617,14 @@ type ShodanConfigurationOptions struct { Targets []string } +type JiraConfigurationOptions struct { + Host string + Email string + DefaultProject string + AutoCloseTickets bool + AutoCreateCases bool +} + type EmailConfigurationOptions struct { Recipients []EmailRecipient AutoCreateTickets bool @@ -639,6 +647,7 @@ type ClientIntegrationConfigurationOptions struct { GithubConfigurationOptions GithubConfigurationOptions `graphql:"... on GithubConfigurationOptions"` HostedAwsConfigurationOptions HostedAwsConfigurationOptions `graphql:"... on HostedAwsConfigurationOptions"` ShodanConfigurationOptions ShodanConfigurationOptions `graphql:"... on ShodanConfigurationOptions"` + JiraConfigurationOptions JiraConfigurationOptions `graphql:"... on JiraConfigurationOptions"` EmailConfigurationOptions EmailConfigurationOptions `graphql:"... on EmailConfigurationOptions"` GitlabConfigurationOptions GitlabConfigurationOptions `graphql:"... on GitlabConfigurationOptions"` // Add other configuration options here diff --git a/internal/provider/integration_jira_resource.go b/internal/provider/integration_jira_resource.go new file mode 100644 index 0000000..6455a79 --- /dev/null +++ b/internal/provider/integration_jira_resource.go @@ -0,0 +1,295 @@ +package provider + +import ( + "context" + "fmt" + "regexp" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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" + mondoov1 "go.mondoo.com/mondoo-go" +) + +var _ resource.Resource = (*integrationJiraResource)(nil) + +func NewIntegrationJiraResource() resource.Resource { + return &integrationJiraResource{} +} + +type integrationJiraResource struct { + client *ExtendedGqlClient +} + +type integrationJiraResourceModel struct { + SpaceID types.String `tfsdk:"space_id"` + + // integration details + Mrn types.String `tfsdk:"mrn"` + Name types.String `tfsdk:"name"` + Host types.String `tfsdk:"host"` + Email types.String `tfsdk:"email"` + + // Optional settings + DefaultProject types.String `tfsdk:"default_project"` + AutoCreate types.Bool `tfsdk:"auto_create"` + AutoClose types.Bool `tfsdk:"auto_close"` + + // credentials + Credential *integrationJiraCredentialModel `tfsdk:"credentials"` +} + +type integrationJiraCredentialModel struct { + Token types.String `tfsdk:"token"` +} + +func (r *integrationJiraResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_integration_jira" +} + +func (m integrationJiraResourceModel) GetConfigurationOptions() *mondoov1.JiraConfigurationOptionsInput { + opts := &mondoov1.JiraConfigurationOptionsInput{ + Host: mondoov1.String(m.Host.ValueString()), + Email: mondoov1.String(m.Email.ValueString()), + APIToken: mondoov1.String(m.Credential.Token.ValueString()), + DefaultProject: mondoov1.String(m.DefaultProject.ValueString()), + AutoCreateCases: mondoov1.NewBooleanPtr(mondoov1.Boolean(m.AutoCreate.ValueBool())), + AutoCloseTickets: mondoov1.NewBooleanPtr(mondoov1.Boolean(m.AutoClose.ValueBool())), + } + + return opts +} + +func (r *integrationJiraResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Integrate the Ticketing System Jira with Mondoo to automatically create and close issues based on Mondoo findings.`, + Attributes: map[string]schema.Attribute{ + "space_id": schema.StringAttribute{ + MarkdownDescription: "Mondoo Space Identifier. If it is not provided, the provider space is used.", + Optional: true, + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "mrn": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Integration identifier.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name of the integration.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(250), + }, + }, + "host": schema.StringAttribute{ + MarkdownDescription: "Jira host URL.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^https?:\/\/[a-zA-Z0-9\-._~:\/?#[\]@!$&'()*+,;=%]+$`), + "must be a valid URL", + ), + }, + }, + "email": schema.StringAttribute{ + MarkdownDescription: "Jira user email.", + Required: true, + Validators: []validator.String{ + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`), + "must be a valid email", + ), + }, + }, + "default_project": schema.StringAttribute{ + MarkdownDescription: "Default Jira project (is represented by the project key e.g. `MONDOO`).", + Optional: true, + }, + "auto_create": schema.BoolAttribute{ + MarkdownDescription: "Automatically create Jira issues for Mondoo findings.", + Optional: true, + }, + "auto_close": schema.BoolAttribute{ + MarkdownDescription: "Automatically close Jira issues for resolved Mondoo findings", + Optional: true, + }, + "credentials": schema.SingleNestedAttribute{ + Required: true, + Attributes: map[string]schema.Attribute{ + "token": schema.StringAttribute{ + MarkdownDescription: "Jira API token.", + Required: true, + Sensitive: true, + }, + }, + }, + }, + } +} + +func (r *integrationJiraResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*ExtendedGqlClient) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *integrationJiraResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data integrationJiraResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Compute and validate the space + space, err := r.client.ComputeSpace(data.SpaceID) + if err != nil { + resp.Diagnostics.AddError("Invalid Configuration", err.Error()) + return + } + ctx = tflog.SetField(ctx, "space_mrn", space.MRN()) + + // Do GraphQL request to API to create the resource. + tflog.Debug(ctx, "Creating integration") + integration, err := r.client.CreateIntegration(ctx, + space.MRN(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeTicketSystemJira, + mondoov1.ClientIntegrationConfigurationInput{ + JiraConfigurationOptions: data.GetConfigurationOptions(), + }) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to create Jira integration, got error: %s", err), + ) + return + } + + // Save space mrn into the Terraform state. + data.Mrn = types.StringValue(string(integration.Mrn)) + data.Name = types.StringValue(string(integration.Name)) + data.SpaceID = types.StringValue(space.ID()) + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationJiraResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data integrationJiraResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Read API call logic + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationJiraResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data integrationJiraResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + opts := mondoov1.ClientIntegrationConfigurationInput{ + JiraConfigurationOptions: data.GetConfigurationOptions(), + } + + _, err := r.client.UpdateIntegration(ctx, + data.Mrn.ValueString(), + data.Name.ValueString(), + mondoov1.ClientIntegrationTypeTicketSystemJira, + opts, + ) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to update Jira integration, got error: %s", err), + ) + return + } + + // Save updated data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *integrationJiraResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data integrationJiraResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + // Do GraphQL request to API to update the resource. + _, err := r.client.DeleteIntegration(ctx, data.Mrn.ValueString()) + if err != nil { + resp.Diagnostics. + AddError("Client Error", + fmt.Sprintf("Unable to delete Jira integration, got error: %s", err), + ) + return + } +} + +func (r *integrationJiraResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + integration, ok := r.client.ImportIntegration(ctx, req, resp) + if !ok { + return + } + + model := integrationJiraResourceModel{ + Mrn: types.StringValue(integration.Mrn), + Name: types.StringValue(integration.Name), + SpaceID: types.StringValue(integration.SpaceID()), + Host: types.StringValue(integration.ConfigurationOptions.JiraConfigurationOptions.Host), + Email: types.StringValue(integration.ConfigurationOptions.JiraConfigurationOptions.Email), + DefaultProject: types.StringValue(integration.ConfigurationOptions.JiraConfigurationOptions.DefaultProject), + AutoCreate: types.BoolValue(integration.ConfigurationOptions.JiraConfigurationOptions.AutoCreateCases), + AutoClose: types.BoolValue(integration.ConfigurationOptions.JiraConfigurationOptions.AutoCloseTickets), + Credential: &integrationJiraCredentialModel{ + Token: types.StringPointerValue(nil), + }, + } + + resp.State.Set(ctx, &model) +} diff --git a/internal/provider/integration_jira_resource_test.go b/internal/provider/integration_jira_resource_test.go new file mode 100644 index 0000000..c30cb3c --- /dev/null +++ b/internal/provider/integration_jira_resource_test.go @@ -0,0 +1,103 @@ +// Copyright (c) Mondoo, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/helper/resource" +) + +func TestAccJiraResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: testAccJiraResourceConfig(accSpace.ID(), "one", "https://your-instance.atlassian.net", "jira.owner@email.com", "MONDOO"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "host", "https://your-instance.atlassian.net"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "email", "jira.owner@email.com"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "default_project", "MONDOO"), + ), + }, + { + Config: testAccJiraResourceWithSpaceInProviderConfig(accSpace.ID(), "two", "abctoken12345", true, false), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "credentials.token", "abctoken12345"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "auto_create", "true"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "auto_close", "false"), + ), + }, + // Update and Read testing + { + Config: testAccJiraResourceConfig(accSpace.ID(), "one", "https://your-instance.atlassian.net", "jira.owner@email.com", "MONDOO"), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "name", "one"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "host", "https://your-instance.atlassian.net"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "email", "jira.owner@email.com"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "default_project", "MONDOO"), + ), + }, + { + Config: testAccJiraResourceWithSpaceInProviderConfig(accSpace.ID(), "two", "abctoken12345", false, true), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "name", "two"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "space_id", accSpace.ID()), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "credentials.token", "abctoken12345"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "auto_create", "false"), + resource.TestCheckResourceAttr("mondoo_integration_jira.test", "auto_close", "true"), + ), + }, + // Delete testing automatically occurs in TestCase + }, + }) +} + +func testAccJiraResourceConfig(spaceID, intName, host, email, defaultProject string) string { + return fmt.Sprintf(` +resource "mondoo_integration_jira" "test" { + space_id = %[1]q + name = %[2]q + host = %[3]q + email = %[4]q + default_project = %[5]q + + auto_create = true + auto_close = true + + credentials = { + token = "abcd1234567890" + } +} +`, spaceID, intName, host, email, defaultProject) +} + +func testAccJiraResourceWithSpaceInProviderConfig(spaceID, intName, token string, autoCreate, autoClose bool) string { + return fmt.Sprintf(` +provider "mondoo" { + space = %[1]q +} +resource "mondoo_integration_jira" "test" { + name = %[2]q + host = "https://your-instance.atlassian.net" + email = "jira.owner@email.com" + default_project = "MONDOO" + + auto_create = %[4]t + auto_close = %[5]t + + credentials = { + token = %[3]q + } +} +`, spaceID, intName, token, autoCreate, autoClose) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4995328..bbb21dc 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -204,6 +204,7 @@ func (p *MondooProvider) Resources(ctx context.Context) []func() resource.Resour NewIntegrationShodanResource, NewFrameworkAssignmentResource, NewCustomFrameworkResource, + NewIntegrationJiraResource, NewIntegrationEmailResource, NewIntegrationGitlabResource, } From bb85c2011074bd3aa07c00de55a4840ed3d6aaab Mon Sep 17 00:00:00 2001 From: Jeff Deininger <9385180+jeff-d@users.noreply.github.com> Date: Fri, 13 Dec 2024 00:38:35 -0500 Subject: [PATCH 6/6] docs: remove erroneous Google reference (#181) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: remove erroneous Google reference Signed-off-by: Jeff Deininger <9385180+jeff-d@users.noreply.github.com> * 🧹 regenerate content --------- Signed-off-by: Jeff Deininger <9385180+jeff-d@users.noreply.github.com> Co-authored-by: Christoph Hartmann