From 694df791f35868998fd850addfac898eed938eb5 Mon Sep 17 00:00:00 2001 From: Pavel Sorokin <60606414+pavel-snyk@users.noreply.github.com> Date: Sat, 17 Sep 2022 22:49:56 +0200 Subject: [PATCH 1/2] chore: update snyk-sdk-go to v0.3.1 --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2fdb77b..54f923d 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/terraform-plugin-go v0.14.0 github.com/hashicorp/terraform-plugin-log v0.7.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.23.0 - github.com/pavel-snyk/snyk-sdk-go v0.1.0 + github.com/pavel-snyk/snyk-sdk-go v0.3.1 github.com/stretchr/testify v1.8.0 ) diff --git a/go.sum b/go.sum index e2bd7af..9d4bb2e 100644 --- a/go.sum +++ b/go.sum @@ -175,8 +175,8 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLA github.com/nsf/jsondiff v0.0.0-20200515183724-f29ed568f4ce h1:RPclfga2SEJmgMmz2k+Mg7cowZ8yv4Trqw9UsJby758= github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= -github.com/pavel-snyk/snyk-sdk-go v0.1.0 h1:k615zmCpVCLMcEW+RyV5EqFaBmOEUM4Ny5QTcSKotGA= -github.com/pavel-snyk/snyk-sdk-go v0.1.0/go.mod h1:LRL1TRuuM925gnyGp54WtS9p8S4yJMd0oS4JpLg+n7Y= +github.com/pavel-snyk/snyk-sdk-go v0.3.1 h1:RechIZ/uzShWUDkEzgt5Bu3CCLeg7q/I09AWBL9uhHQ= +github.com/pavel-snyk/snyk-sdk-go v0.3.1/go.mod h1:LRL1TRuuM925gnyGp54WtS9p8S4yJMd0oS4JpLg+n7Y= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= From 2dd736de5a3174f1d49e874165d47377cfab13c1 Mon Sep 17 00:00:00 2001 From: Pavel Sorokin <60606414+pavel-snyk@users.noreply.github.com> Date: Sun, 18 Sep 2022 19:01:29 +0200 Subject: [PATCH 2/2] feat(resource/snyk_integration): add configuration options for pull request testing --- docs/resources/integration.md | 31 +++- .../resources/snyk_integration/resource.tf | 6 - .../snyk_integration/resource_default.tf | 6 + .../resource_with_pull_request_testing.tf | 13 ++ internal/provider/resource_integration.go | 135 ++++++++++++++++-- .../provider/resource_integration_test.go | 49 +++++++ internal/provider/types_conversion.go | 17 +++ internal/provider/types_conversion_test.go | 78 ++++++++++ templates/resources/integration.md.tmpl | 23 +++ 9 files changed, 341 insertions(+), 17 deletions(-) delete mode 100644 examples/resources/snyk_integration/resource.tf create mode 100644 examples/resources/snyk_integration/resource_default.tf create mode 100644 examples/resources/snyk_integration/resource_with_pull_request_testing.tf create mode 100644 internal/provider/types_conversion.go create mode 100644 internal/provider/types_conversion_test.go create mode 100644 templates/resources/integration.md.tmpl diff --git a/docs/resources/integration.md b/docs/resources/integration.md index 01367a2..ca1aeef 100644 --- a/docs/resources/integration.md +++ b/docs/resources/integration.md @@ -12,12 +12,32 @@ The integration resource allows you to manage Snyk integration. ## Example Usage +### Default + ```terraform -resource "snyk_integration" "github" { +resource "snyk_integration" "gitlab" { organization_id = snyk_organization.frontend.id + type = "gitlab" + token = "gitlab-secret-token" +} +``` + +### Using Pull Request Testing + +```terraform +resource "snyk_integration" "github" { + organization_id = snyk_organization.backend.id + type = "github" token = "rotated-github-secret-token" + + pull_request_testing = { + enabled = true + + fail_on_any_issue = false + fail_only_on_issues_with_fix = true + } } ``` @@ -32,6 +52,7 @@ resource "snyk_integration" "github" { ### Optional - `password` (String, Sensitive) The password used by the integration. +- `pull_request_testing` (Attributes) Pull request tests settings applied whenever a new PR is opened. (see [below for nested schema](#nestedatt--pull_request_testing)) - `region` (String) The region used by the integration. - `registry_url` (String) The URL for container registries used by the integration (e.g. for ECR). - `role_arn` (String) The role ARN used by the integration (ECR only). @@ -43,4 +64,12 @@ resource "snyk_integration" "github" { - `id` (String) The ID of the integration. + +### Nested Schema for `pull_request_testing` + +Optional: +- `enabled` (Boolean) Denotes the pull request testing feature should be enabled for this integration. +- `fail_on_any_issue` (Boolean) Fails an opened pull request if any vulnerable dependencies have been detected, otherwise the pull request should only fail when a dependency with issues is added. +- `fail_only_for_high_and_critical_severity` (Boolean) Fails an opened pull request if any dependencies are marked as being of high or critical severity. +- `fail_only_on_issues_with_fix` (Boolean) Fails an opened pull request only when issues found have a fix available. diff --git a/examples/resources/snyk_integration/resource.tf b/examples/resources/snyk_integration/resource.tf deleted file mode 100644 index b20c1ad..0000000 --- a/examples/resources/snyk_integration/resource.tf +++ /dev/null @@ -1,6 +0,0 @@ -resource "snyk_integration" "github" { - organization_id = snyk_organization.frontend.id - - type = "github" - token = "rotated-github-secret-token" -} diff --git a/examples/resources/snyk_integration/resource_default.tf b/examples/resources/snyk_integration/resource_default.tf new file mode 100644 index 0000000..6a8613a --- /dev/null +++ b/examples/resources/snyk_integration/resource_default.tf @@ -0,0 +1,6 @@ +resource "snyk_integration" "gitlab" { + organization_id = snyk_organization.frontend.id + + type = "gitlab" + token = "gitlab-secret-token" +} diff --git a/examples/resources/snyk_integration/resource_with_pull_request_testing.tf b/examples/resources/snyk_integration/resource_with_pull_request_testing.tf new file mode 100644 index 0000000..e8cbce9 --- /dev/null +++ b/examples/resources/snyk_integration/resource_with_pull_request_testing.tf @@ -0,0 +1,13 @@ +resource "snyk_integration" "github" { + organization_id = snyk_organization.backend.id + + type = "github" + token = "rotated-github-secret-token" + + pull_request_testing = { + enabled = true + + fail_on_any_issue = false + fail_only_on_issues_with_fix = true + } +} diff --git a/internal/provider/resource_integration.go b/internal/provider/resource_integration.go index d9ba4b8..37360de 100644 --- a/internal/provider/resource_integration.go +++ b/internal/provider/resource_integration.go @@ -44,6 +44,51 @@ func (r integrationResourceType) GetSchema(_ context.Context) (tfsdk.Schema, dia }, Type: types.StringType, }, + "pull_request_testing": { + Description: "Pull request tests settings applied whenever a new PR is opened.", + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + Attributes: tfsdk.SingleNestedAttributes(map[string]tfsdk.Attribute{ + "enabled": { + Description: "Denotes the pull request testing feature should be enabled for this integration.", + Computed: true, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + Type: types.BoolType, + }, + "fail_on_any_issue": { + Description: "Fails an opened pull request if any vulnerable dependencies have been detected, otherwise the pull request should only fail when a dependency with issues is added.", + Computed: true, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + Type: types.BoolType, + }, + "fail_only_for_high_and_critical_severity": { + Description: "Fails an opened pull request if any dependencies are marked as being of high or critical severity.", + Computed: true, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + Type: types.BoolType, + }, + "fail_only_on_issues_with_fix": { + Description: "Fails an opened pull request only when issues found have a fix available.", + Computed: true, + Optional: true, + PlanModifiers: tfsdk.AttributePlanModifiers{ + resource.UseStateForUnknown(), + }, + Type: types.BoolType, + }, + }), + }, "region": { Description: "The region used by the integration.", Optional: true, @@ -117,16 +162,24 @@ type integrationResource struct { } type integrationData struct { - ID types.String `tfsdk:"id"` - OrganizationID types.String `tfsdk:"organization_id"` - Password types.String `tfsdk:"password"` - Region types.String `tfsdk:"region"` - RegistryURL types.String `tfsdk:"registry_url"` - RoleARN types.String `tfsdk:"role_arn"` - Token types.String `tfsdk:"token"` - Type types.String `tfsdk:"type"` - URL types.String `tfsdk:"url"` - Username types.String `tfsdk:"username"` + ID types.String `tfsdk:"id"` + OrganizationID types.String `tfsdk:"organization_id"` + Password types.String `tfsdk:"password"` + PullRequestTesting *pullRequestTesting `tfsdk:"pull_request_testing"` + Region types.String `tfsdk:"region"` + RegistryURL types.String `tfsdk:"registry_url"` + RoleARN types.String `tfsdk:"role_arn"` + Token types.String `tfsdk:"token"` + Type types.String `tfsdk:"type"` + URL types.String `tfsdk:"url"` + Username types.String `tfsdk:"username"` +} + +type pullRequestTesting struct { + Enabled types.Bool `tfsdk:"enabled"` + FailOnAnyIssue types.Bool `tfsdk:"fail_on_any_issue"` + FailOnlyForHighAndCriticalSeverity types.Bool `tfsdk:"fail_only_for_high_and_critical_severity"` + FailOnlyOnIssuesWithFix types.Bool `tfsdk:"fail_only_on_issues_with_fix"` } func (r integrationResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { @@ -212,6 +265,29 @@ func (r integrationResource) Create(ctx context.Context, request resource.Create result.ID = types.String{Value: integration.ID} } + if plan.PullRequestTesting != nil { + updateRequest := &snyk.IntegrationSettingsUpdateRequest{ + IntegrationSettings: &snyk.IntegrationSettings{ + PullRequestTestEnabled: toBoolPtr(plan.PullRequestTesting.Enabled), + PullRequestFailOnAnyVulnerability: toBoolPtr(plan.PullRequestTesting.FailOnAnyIssue), + PullRequestFailOnlyForHighAndCriticalSeverity: toBoolPtr(plan.PullRequestTesting.FailOnlyForHighAndCriticalSeverity), + PullRequestFailOnlyForIssuesWithFix: toBoolPtr(plan.PullRequestTesting.FailOnlyOnIssuesWithFix), + }, + } + + settings, _, err := r.p.client.Integrations.UpdateSettings(ctx, orgID, result.ID.Value, updateRequest) + if err != nil { + response.Diagnostics.AddError("Error updating pull request settings", err.Error()) + return + } + result.PullRequestTesting = &pullRequestTesting{ + Enabled: fromBoolPtr(settings.PullRequestTestEnabled), + FailOnAnyIssue: fromBoolPtr(settings.PullRequestFailOnAnyVulnerability), + FailOnlyForHighAndCriticalSeverity: fromBoolPtr(settings.PullRequestFailOnlyForHighAndCriticalSeverity), + FailOnlyOnIssuesWithFix: fromBoolPtr(settings.PullRequestFailOnlyForIssuesWithFix), + } + } + diags = response.State.Set(ctx, result) response.Diagnostics.Append(diags...) if response.Diagnostics.HasError() { @@ -235,6 +311,22 @@ func (r integrationResource) Read(ctx context.Context, request resource.ReadRequ return } + if state.PullRequestTesting != nil { + settings, _, err := r.p.client.Integrations.GetSettings(ctx, organizationID, integration.ID) + if err != nil { + response.Diagnostics.AddError("Error reading integration settings", err.Error()) + return + } + + pullRequestTesting := &pullRequestTesting{ + Enabled: fromBoolPtr(settings.PullRequestTestEnabled), + FailOnAnyIssue: fromBoolPtr(settings.PullRequestFailOnAnyVulnerability), + FailOnlyForHighAndCriticalSeverity: fromBoolPtr(settings.PullRequestFailOnlyForHighAndCriticalSeverity), + FailOnlyOnIssuesWithFix: fromBoolPtr(settings.PullRequestFailOnlyForIssuesWithFix), + } + state.PullRequestTesting = pullRequestTesting + } + state.ID = types.String{Value: integration.ID} diags = response.State.Set(ctx, &state) @@ -275,6 +367,29 @@ func (r integrationResource) Update(ctx context.Context, request resource.Update return } + if plan.PullRequestTesting != nil { + updateRequest := &snyk.IntegrationSettingsUpdateRequest{ + IntegrationSettings: &snyk.IntegrationSettings{ + PullRequestTestEnabled: toBoolPtr(plan.PullRequestTesting.Enabled), + PullRequestFailOnAnyVulnerability: toBoolPtr(plan.PullRequestTesting.FailOnAnyIssue), + PullRequestFailOnlyForHighAndCriticalSeverity: toBoolPtr(plan.PullRequestTesting.FailOnlyForHighAndCriticalSeverity), + PullRequestFailOnlyForIssuesWithFix: toBoolPtr(plan.PullRequestTesting.FailOnlyOnIssuesWithFix), + }, + } + + settings, _, err := r.p.client.Integrations.UpdateSettings(ctx, organizationID, integrationID, updateRequest) + if err != nil { + response.Diagnostics.AddError("Error updating pull request settings", err.Error()) + return + } + plan.PullRequestTesting = &pullRequestTesting{ + Enabled: fromBoolPtr(settings.PullRequestTestEnabled), + FailOnAnyIssue: fromBoolPtr(settings.PullRequestFailOnAnyVulnerability), + FailOnlyForHighAndCriticalSeverity: fromBoolPtr(settings.PullRequestFailOnlyForHighAndCriticalSeverity), + FailOnlyOnIssuesWithFix: fromBoolPtr(settings.PullRequestFailOnlyForIssuesWithFix), + } + } + plan.ID = types.String{Value: integration.ID} diags = response.State.Set(ctx, &plan) diff --git a/internal/provider/resource_integration_test.go b/internal/provider/resource_integration_test.go index 3ab0b68..970e8a9 100644 --- a/internal/provider/resource_integration_test.go +++ b/internal/provider/resource_integration_test.go @@ -35,6 +35,32 @@ func TestAccResourceIntegration_basic(t *testing.T) { testAccCheckResourceIntegrationExists("snyk_integration.test", organizationName, &integration), resource.TestCheckResourceAttrSet("snyk_integration.test", "id"), resource.TestCheckResourceAttr("snyk_integration.test", "type", "gitlab"), + resource.TestCheckNoResourceAttr("snyk_integration.test", "pull_request_testing.enabled"), + ), + }, + }, + }) +} + +func TestAccResourceIntegration_pullRequestTesting(t *testing.T) { + t.Parallel() + + var integration snyk.Integration + organizationName := fmt.Sprintf("tf-test-acc_%s", acctest.RandString(10)) + groupID := os.Getenv("SNYK_GROUP_ID") + token := acctest.RandString(20) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccResourceIntegrationConfigWithPullRequestTesting(organizationName, groupID, token), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckResourceIntegrationExists("snyk_integration.test", organizationName, &integration), + resource.TestCheckResourceAttrSet("snyk_integration.test", "pull_request_testing.enabled"), + resource.TestCheckResourceAttr("snyk_integration.test", "pull_request_testing.fail_on_any_issue", "true"), + resource.TestCheckResourceAttr("snyk_integration.test", "pull_request_testing.fail_only_on_issues_with_fix", "false"), ), }, }, @@ -101,3 +127,26 @@ resource "snyk_integration" "test" { } `, organizationName, groupID, token) } + +func testAccResourceIntegrationConfigWithPullRequestTesting(organizationName, groupID, token string) string { + return fmt.Sprintf(` +resource "snyk_organization" "test" { + name = "%s" + group_id = "%s" +} +resource "snyk_integration" "test" { + organization_id = snyk_organization.test.id + + type = "gitlab" + url = "https://testing.gitlab.local" + token = "%s" + + pull_request_testing = { + enabled = true + + fail_on_any_issue = true + fail_only_on_issues_with_fix = false + } +} +`, organizationName, groupID, token) +} diff --git a/internal/provider/types_conversion.go b/internal/provider/types_conversion.go new file mode 100644 index 0000000..e3e945b --- /dev/null +++ b/internal/provider/types_conversion.go @@ -0,0 +1,17 @@ +package provider + +import "github.com/hashicorp/terraform-plugin-framework/types" + +func fromBoolPtr(v *bool) types.Bool { + if v == nil { + return types.Bool{Null: true} + } + return types.Bool{Value: *v} +} + +func toBoolPtr(v types.Bool) *bool { + if v.Null || v.Unknown { + return nil + } + return &v.Value +} diff --git a/internal/provider/types_conversion_test.go b/internal/provider/types_conversion_test.go new file mode 100644 index 0000000..1dd3140 --- /dev/null +++ b/internal/provider/types_conversion_test.go @@ -0,0 +1,78 @@ +package provider + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" +) + +func TestTypeConversion_fromBool(t *testing.T) { + t.Parallel() + + valTrue := true + valFalse := false + + type testCase struct { + val *bool + expected types.Bool + } + tests := map[string]testCase{ + "nil": { + val: nil, + expected: types.Bool{Null: true}, + }, + "true": { + val: &valTrue, + expected: types.Bool{Value: valTrue}, + }, + "false": { + val: &valFalse, + expected: types.Bool{Value: valFalse}, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := fromBoolPtr(test.val) + assert.Equal(t, test.expected, actual) + }) + } +} + +func TestTypeConversion_toBool(t *testing.T) { + t.Parallel() + + expectedTrue := true + expectedFalse := false + + type testCase struct { + val types.Bool + expected *bool + } + tests := map[string]testCase{ + "null_bool": { + val: types.Bool{Null: true}, + expected: nil, + }, + "unknown_bool": { + val: types.Bool{Unknown: true}, + expected: nil, + }, + "true_bool": { + val: types.Bool{Value: true}, + expected: &expectedTrue, + }, + "false_bool": { + val: types.Bool{Value: false}, + expected: &expectedFalse, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + actual := toBoolPtr(test.val) + assert.Equal(t, test.expected, actual) + }) + } +} diff --git a/templates/resources/integration.md.tmpl b/templates/resources/integration.md.tmpl new file mode 100644 index 0000000..dc98ada --- /dev/null +++ b/templates/resources/integration.md.tmpl @@ -0,0 +1,23 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +{{ .Description | trimspace }} + +## Example Usage + +### Default + +{{ tffile "examples/resources/snyk_integration/resource_default.tf" }} + +### Using Pull Request Testing + +{{ tffile "examples/resources/snyk_integration/resource_with_pull_request_testing.tf" }} + +{{ .SchemaMarkdown | trimspace }}