From 64582ae980514db8bd3cf1ffa874a638203a12a8 Mon Sep 17 00:00:00 2001 From: Michal Wasilewski Date: Mon, 8 Jan 2024 18:50:29 +0100 Subject: [PATCH] feat(audit-trail): add support for audit trail to terraform provider Signed-off-by: Michal Wasilewski --- docs/resources/audit_trail_webhook.md | 38 ++++++ .../spacelift_audit_trail_webhook/resource.tf | 5 + .../internal/structs/audit_trail_webhook.go | 8 ++ .../structs/audit_trail_webhook_input.go | 10 ++ spacelift/provider.go | 1 + spacelift/resource_audit_trail_webhook.go | 128 ++++++++++++++++++ .../resource_audit_trail_webhook_test.go | 53 ++++++++ 7 files changed, 243 insertions(+) create mode 100644 docs/resources/audit_trail_webhook.md create mode 100644 examples/resources/spacelift_audit_trail_webhook/resource.tf create mode 100644 spacelift/internal/structs/audit_trail_webhook.go create mode 100644 spacelift/internal/structs/audit_trail_webhook_input.go create mode 100644 spacelift/resource_audit_trail_webhook.go create mode 100644 spacelift/resource_audit_trail_webhook_test.go diff --git a/docs/resources/audit_trail_webhook.md b/docs/resources/audit_trail_webhook.md new file mode 100644 index 00000000..4c5fa483 --- /dev/null +++ b/docs/resources/audit_trail_webhook.md @@ -0,0 +1,38 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "spacelift_audit_trail_webhook Resource - terraform-provider-spacelift" +subcategory: "" +description: |- + spacelift_audit_trail_webhook represents a webhook endpoint to which Spacelift sends POST requests about audit events. +--- + +# spacelift_audit_trail_webhook (Resource) + +`spacelift_audit_trail_webhook` represents a webhook endpoint to which Spacelift sends POST requests about audit events. + +## Example Usage + +```terraform +resource "spacelift_audit_trail_webhook" "example" { + endpoint = "https://example.com" + enabled = true + secret = "mysecretkey" +} +``` + + +## Schema + +### Required + +- `enabled` (Boolean) `enabled` determines whether the webhook is enabled. If it is not, Spacelift will not send any requests to the endpoint. +- `endpoint` (String) `endpoint` is the URL to which Spacelift will send POST requests about audit events. +- `secret` (String, Sensitive) `secret` is a secret that Spacelift will send with the request + +### Optional + +- `include_runs` (Boolean) `include_runs` determines whether the webhook should include information about the run that triggered the event. + +### Read-Only + +- `id` (String) The ID of this resource. diff --git a/examples/resources/spacelift_audit_trail_webhook/resource.tf b/examples/resources/spacelift_audit_trail_webhook/resource.tf new file mode 100644 index 00000000..75ec5881 --- /dev/null +++ b/examples/resources/spacelift_audit_trail_webhook/resource.tf @@ -0,0 +1,5 @@ +resource "spacelift_audit_trail_webhook" "example" { + endpoint = "https://example.com" + enabled = true + secret = "mysecretkey" +} diff --git a/spacelift/internal/structs/audit_trail_webhook.go b/spacelift/internal/structs/audit_trail_webhook.go new file mode 100644 index 00000000..181eaf0e --- /dev/null +++ b/spacelift/internal/structs/audit_trail_webhook.go @@ -0,0 +1,8 @@ +package structs + +type AuditTrailWebhook struct { + Enabled bool `graphql:"enabled"` + Endpoint string `graphql:"endpoint"` + IncludeRuns bool `graphql:"includeRuns"` + Secret string `graphql:"secret"` +} diff --git a/spacelift/internal/structs/audit_trail_webhook_input.go b/spacelift/internal/structs/audit_trail_webhook_input.go new file mode 100644 index 00000000..f5c2b6b6 --- /dev/null +++ b/spacelift/internal/structs/audit_trail_webhook_input.go @@ -0,0 +1,10 @@ +package structs + +import "github.com/shurcooL/graphql" + +type AuditTrailWebhookInput struct { + Enabled graphql.Boolean `json:"enabled"` + Endpoint graphql.String `json:"endpoint"` + IncludeRuns graphql.Boolean `json:"includeRuns"` + Secret graphql.String `json:"secret"` +} diff --git a/spacelift/provider.go b/spacelift/provider.go index ebedbf06..0af58320 100644 --- a/spacelift/provider.go +++ b/spacelift/provider.go @@ -98,6 +98,7 @@ func Provider(commit, version string) plugin.ProviderFunc { "spacelift_worker_pools": dataWorkerPools(), }, ResourcesMap: map[string]*schema.Resource{ + "spacelift_audit_trail_webhook": resourceAuditTrailWebhook(), "spacelift_aws_role": resourceAWSRole(), "spacelift_aws_integration": resourceAWSIntegration(), "spacelift_aws_integration_attachment": resourceAWSIntegrationAttachment(), diff --git a/spacelift/resource_audit_trail_webhook.go b/spacelift/resource_audit_trail_webhook.go new file mode 100644 index 00000000..23523521 --- /dev/null +++ b/spacelift/resource_audit_trail_webhook.go @@ -0,0 +1,128 @@ +package spacelift + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal" + "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/structs" +) + +func resourceAuditTrailWebhook() *schema.Resource { + return &schema.Resource{ + Description: "" + + "`spacelift_audit_trail_webhook` represents a webhook endpoint to which Spacelift " + + "sends POST requests about audit events.", + CreateContext: resourceAuditTrailWebhookCreate, + ReadContext: resourceAuditTrailWebhookRead, + UpdateContext: resourceAuditTrailWebhookUpdate, + DeleteContext: resourceAuditTrailWebhookDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + + Schema: map[string]*schema.Schema{ + "enabled": { + Type: schema.TypeBool, + Required: true, + Description: "`enabled` determines whether the webhook is enabled. If it is not, " + + "Spacelift will not send any requests to the endpoint.", + }, + "endpoint": { + Type: schema.TypeString, + Required: true, + Description: "`endpoint` is the URL to which Spacelift will send POST requests " + + "about audit events.", + }, + "include_runs": { + Type: schema.TypeBool, + Optional: true, + Description: "`include_runs` determines whether the webhook should include " + + "information about the run that triggered the event.", + }, + "secret": { + Type: schema.TypeString, + Required: true, + Sensitive: true, + Description: "`secret` is a secret that Spacelift will send with the request", + }, + }, + } +} + +func resourceAuditTrailWebhookCreate(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + var mutation struct { + AuditTrailWebhook *structs.AuditTrailWebhook `graphql:"auditTrailSetWebhook(input: $input)"` + } + variables := map[string]interface{}{ + "input": structs.AuditTrailWebhookInput{ + Enabled: toBool(data.Get("enabled")), + Endpoint: toString(data.Get("endpoint")), + IncludeRuns: toBool(data.Get("include_runs")), + Secret: toString(data.Get("secret")), + }, + } + if err := i.(*internal.Client).Mutate(ctx, "AuditTrailWebhookCreate", &mutation, variables); err != nil { + return diag.Errorf("could not create audit trail webhook: %v", internal.FromSpaceliftError(err)) + } + + data.SetId(time.Now().String()) + + return resourceAuditTrailWebhookRead(ctx, data, i) +} + +func resourceAuditTrailWebhookRead(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + var query struct { + AuditTrailWebhook *structs.AuditTrailWebhook `graphql:"auditTrailWebhook"` + } + if err := i.(*internal.Client).Query(ctx, "AuditTrailWebhookRead", &query, nil); err != nil { + return diag.Errorf("could not query for audit trail webhook: %v", internal.FromSpaceliftError(err)) + } + + if query.AuditTrailWebhook == nil { + data.SetId("") + return nil + } + + data.Set("enabled", query.AuditTrailWebhook.Enabled) + data.Set("endpoint", query.AuditTrailWebhook.Endpoint) + data.Set("include_runs", query.AuditTrailWebhook.IncludeRuns) + data.Set("secret", query.AuditTrailWebhook.Secret) + + return nil +} + +func resourceAuditTrailWebhookUpdate(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + var mutation struct { + AuditTrailWebhook *structs.AuditTrailWebhook `graphql:"auditTrailSetWebhook(input: $input)"` + } + variables := map[string]interface{}{ + "input": structs.AuditTrailWebhookInput{ + Enabled: toBool(data.Get("enabled")), + Endpoint: toString(data.Get("endpoint")), + IncludeRuns: toBool(data.Get("include_runs")), + Secret: toString(data.Get("secret")), + }, + } + if err := i.(*internal.Client).Mutate(ctx, "AuditTrailWebhookUpdate", &mutation, variables); err != nil { + return diag.Errorf("could not update audit trail webhook: %v", internal.FromSpaceliftError(err)) + } + + return resourceAuditTrailWebhookRead(ctx, data, i) +} + +func resourceAuditTrailWebhookDelete(ctx context.Context, data *schema.ResourceData, i interface{}) diag.Diagnostics { + var mutation struct { + AuditTrailWebhook *structs.AuditTrailWebhook `graphql:"auditTrailDeleteWebhook"` + } + if err := i.(*internal.Client).Mutate(ctx, "AuditTrailWebhookDelete", &mutation, nil); err != nil { + return diag.Errorf("could not delete audit trail webhook: %v", internal.FromSpaceliftError(err)) + } + + data.SetId("") + + return nil +} diff --git a/spacelift/resource_audit_trail_webhook_test.go b/spacelift/resource_audit_trail_webhook_test.go new file mode 100644 index 00000000..e840d6e0 --- /dev/null +++ b/spacelift/resource_audit_trail_webhook_test.go @@ -0,0 +1,53 @@ +package spacelift + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/spacelift-io/terraform-provider-spacelift/spacelift/internal/testhelpers" +) + +var auditTrailWebhookSimple = ` +resource "spacelift_audit_trail_webhook" "test" { + enabled = true + endpoint = "%s" + include_runs = true + secret = "secret" +} +` + +func Test_resourceAuditTrailWebhook(t *testing.T) { + const resourceName = "spacelift_audit_trail_webhook.test" + + t.Run("creates an audit trail webhook without an error", func(t *testing.T) { + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(auditTrailWebhookSimple, "https://example.com"), + Check: Resource( + resourceName, + Attribute("enabled", Equals("true")), + Attribute("endpoint", Equals("https://example.com")), + Attribute("include_runs", Equals("true")), + Attribute("secret", Equals("secret")), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }) + }) + + t.Run("endpoint has to exist", func(t *testing.T) { + testSteps(t, []resource.TestStep{ + { + Config: fmt.Sprintf(auditTrailWebhookSimple, "https://invalidendpoint.com/"), + ExpectError: regexp.MustCompile(`could not send webhook to given endpoint`), + }, + }) + }) +}