diff --git a/CHANGELOG.md b/CHANGELOG.md index cc7c362e..4e1e50a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 1.8.0 (October 18, 2022). Tested on Artifactory 7.46.6 and Xray 3.59.4 + +IMPROVEMENTS: + +* resource/xray_watch: add functionality to apply `path_ant_filter` for `repository` and `all-repos` `watch_resource.type`. + PR [#82](https://github.com/jfrog/terraform-provider-xray/pull/82) + ## 1.7.0 (October 6, 2022). Tested on Artifactory 7.41.13 and Xray 3.57.6 NEW FEATURE: diff --git a/docs/resources/watch.md b/docs/resources/watch.md index 50ef2abe..689f55a1 100644 --- a/docs/resources/watch.md +++ b/docs/resources/watch.md @@ -85,6 +85,49 @@ resource "xray_watch" "repository" { watch_recipients = ["test@email.com", "test1@email.com"] } +resource "xray_watch" "repository-ant-filter" { + name = "repository-watch" + description = "Watch a single repo or a list of repositories, using ant pattern" + active = true + project_key = "testproj" + + watch_resource { + type = "repository" + bin_mgr_id = "default" + name = "your-repository-name" + repo_type = "local" + + path_ant_filter { + exclude_patterns = ["**/*.md"] + include_patterns = ["**/*.js"] + } + } + + watch_resource { + type = "repository" + bin_mgr_id = "default" + name = "your-other-repository-name" + repo_type = "remote" + + filter { + type = "regex" + value = ".*" + } + } + + assigned_policy { + name = xray_security_policy.min_severity.name + type = "security" + } + + assigned_policy { + name = xray_license_policy.cvss_range.name + type = "license" + } + + watch_recipients = ["test@email.com", "test1@email.com"] +} + resource "xray_watch" "all-builds-with-filters" { name = "build-watch" description = "Watch all builds with Ant patterns filter" @@ -269,6 +312,7 @@ Optional: - `bin_mgr_id` (String) The ID number of a binary manager resource. Default value is `default`. To check the list of available binary managers, use the API call `${JFROG_URL}/xray/api/v1/binMgr` as an admin user, use `binMgrId` value. More info [here](https://www.jfrog.com/confluence/display/JFROG/Xray+REST+API#XrayRESTAPI-GetBinaryManager) - `filter` (Block Set) Filter for `regex` and `package-type` type. Works only with `all-repos` watch_resource.type. (see [below for nested schema](#nestedblock--watch_resource--filter)) - `name` (String) The name of the build, repository or project. Xray indexing must be enabled on the repository or build +- `path_ant_filter` (Block Set) `path-ant-patterns` filter for `repository` and `all-repos` watch_resource.type (see [below for nested schema](#nestedblock--watch_resource--path_ant_filter)) - `repo_type` (String) Type of repository. Only applicable when `type` is `repository`. Options: `local` or `remote`. @@ -286,4 +330,13 @@ Required: Required: - `type` (String) The type of filter, such as `regex` or `package-type` -- `value` (String) The value of the filter, such as the text of the regex or name of the package type. \ No newline at end of file +- `value` (String) The value of the filter, such as the text of the regex or name of the package type. + + + +### Nested Schema for `watch_resource.path_ant_filter` + +Required: + +- `exclude_patterns` (List of String) List of Ant patterns. +- `include_patterns` (List of String) List of Ant patterns. \ No newline at end of file diff --git a/examples/resources/xray_watch/resource.tf b/examples/resources/xray_watch/resource.tf index 92866810..afee0ddc 100644 --- a/examples/resources/xray_watch/resource.tf +++ b/examples/resources/xray_watch/resource.tf @@ -69,6 +69,49 @@ resource "xray_watch" "repository" { watch_recipients = ["test@email.com", "test1@email.com"] } +resource "xray_watch" "repository-ant-filter" { + name = "repository-watch" + description = "Watch a single repo or a list of repositories, using ant pattern" + active = true + project_key = "testproj" + + watch_resource { + type = "repository" + bin_mgr_id = "default" + name = "your-repository-name" + repo_type = "local" + + path_ant_filter { + exclude_patterns = ["**/*.md"] + include_patterns = ["**/*.js"] + } + } + + watch_resource { + type = "repository" + bin_mgr_id = "default" + name = "your-other-repository-name" + repo_type = "remote" + + filter { + type = "regex" + value = ".*" + } + } + + assigned_policy { + name = xray_security_policy.min_severity.name + type = "security" + } + + assigned_policy { + name = xray_license_policy.cvss_range.name + type = "license" + } + + watch_recipients = ["test@email.com", "test1@email.com"] +} + resource "xray_watch" "all-builds-with-filters" { name = "build-watch" description = "Watch all builds with Ant patterns filter" diff --git a/pkg/xray/resource_xray_watch.go b/pkg/xray/resource_xray_watch.go index 6f16b47b..a06b30b1 100644 --- a/pkg/xray/resource_xray_watch.go +++ b/pkg/xray/resource_xray_watch.go @@ -119,6 +119,34 @@ func resourceXrayWatch() *schema.Resource { }, }, }, + "path_ant_filter": { + Type: schema.TypeSet, + Optional: true, + MinItems: 1, + Description: "`path-ant-patterns` filter for `repository` and `all-repos` watch_resource.type", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "include_patterns": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + MinItems: 1, + Description: "List of Ant patterns.", + }, + "exclude_patterns": { + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + Required: true, + MinItems: 1, + Description: "List of Ant patterns.", + }, + }, + }, + }, }, }, }, diff --git a/pkg/xray/resource_xray_watch_test.go b/pkg/xray/resource_xray_watch_test.go index 4af886ae..1f1ba3bb 100644 --- a/pkg/xray/resource_xray_watch_test.go +++ b/pkg/xray/resource_xray_watch_test.go @@ -2,14 +2,14 @@ package xray import ( "fmt" - "github.com/jfrog/terraform-provider-shared/test" - "github.com/jfrog/terraform-provider-shared/util" "regexp" "testing" "github.com/go-resty/resty/v2" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" "github.com/jfrog/terraform-provider-shared/client" + "github.com/jfrog/terraform-provider-shared/test" + "github.com/jfrog/terraform-provider-shared/util" ) var testDataWatch = map[string]string{ @@ -39,7 +39,7 @@ func TestAccWatch_allReposSinglePolicy(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { - testCheckPolicyDeleted("xray_security_policy.security", t, request) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err }), @@ -53,6 +53,33 @@ func TestAccWatch_allReposSinglePolicy(t *testing.T) { }) } +func TestAccWatch_allReposPathAntFilter(t *testing.T) { + _, fqrn, resourceName := test.MkNames("watch-", "xray_watch") + testData := util.MergeMaps(testDataWatch) + + testData["resource_name"] = resourceName + testData["watch_name"] = fmt.Sprintf("xray-watch-%d", test.RandomInt()) + testData["policy_name_0"] = fmt.Sprintf("xray-policy-%d", test.RandomInt()) + testData["exclude_patterns0"] = "**/*.md" + testData["include_patterns0"] = "**/*.js" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { + testCheckPolicyDeleted(testData["policy_name_0"], t, request) + resp, err := testCheckWatch(id, request) + return resp, err + }), + ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: util.ExecuteTemplate(fqrn, allReposPathAntFilterWatchTemplate, testData), + Check: verifyXrayWatch(fqrn, testData), + }, + }, + }) +} + func TestAccWatch_allReposWithProjectKey(t *testing.T) { _, fqrn, resourceName := test.MkNames("watch-", "xray_watch") projectKey := fmt.Sprintf("testproj%d", test.RandSelect(1, 2, 3, 4, 5)) @@ -156,8 +183,8 @@ func TestAccWatch_allReposMultiplePolicies(t *testing.T) { resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { - testCheckPolicyDeleted("xray_security_policy.security", t, request) - testCheckPolicyDeleted("xray_license_policy.license", t, request) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) + testCheckPolicyDeleted(testData["policy_name_1"], t, request) resp, err := testCheckWatch(id, request) return resp, err }), @@ -206,7 +233,7 @@ func makeSingleRepositoryTestCase(repoType string, t *testing.T) (*testing.T, re }, CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { testAccDeleteRepo(t, testData["repo0"]) - testCheckPolicyDeleted("xray_security_policy.security", t, request) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err }), @@ -343,7 +370,7 @@ func TestAccWatch_repositoryMissingRepoType(t *testing.T) { }, CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { testAccDeleteRepo(t, testData["repo0"]) - testCheckPolicyDeleted("xray_security_policy.security", t, request) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err }), @@ -379,7 +406,7 @@ func TestAccWatch_multipleRepositories(t *testing.T) { CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { testAccDeleteRepo(t, testData["repo0"]) testAccDeleteRepo(t, testData["repo1"]) - testCheckPolicyDeleted("xray_security_policy.security", t, request) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) resp, err := testCheckWatch(id, request) return resp, err }), @@ -393,6 +420,93 @@ func TestAccWatch_multipleRepositories(t *testing.T) { }) } +func TestAccWatch_multipleRepositoriesPathAntPatterns(t *testing.T) { + _, fqrn, resourceName := test.MkNames("watch-", "xray_watch") + testData := util.MergeMaps(testDataWatch) + + testData["resource_name"] = resourceName + testData["watch_name"] = fmt.Sprintf("xray-watch-%d", test.RandomInt()) + testData["policy_name_0"] = fmt.Sprintf("xray-policy-%d", test.RandomInt()) + testData["watch_type"] = "repository" + testData["repo_type"] = "local" + testData["repo0"] = fmt.Sprintf("libs-release-local-0-%d", test.RandomInt()) + testData["repo1"] = fmt.Sprintf("libs-release-local-1-%d", test.RandomInt()) + testData["exclude_patterns0"] = "**/*.md" + testData["include_patterns0"] = "**/*.js" + testData["exclude_patterns1"] = "**/*.txt" + testData["include_patterns1"] = "**/*.jar" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccCreateRepos(t, testData["repo0"], "local", "") + testAccCreateRepos(t, testData["repo1"], "local", "") + }, + CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { + testAccDeleteRepo(t, testData["repo0"]) + testAccDeleteRepo(t, testData["repo1"]) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) + resp, err := testCheckWatch(id, request) + return resp, err + }), + ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: util.ExecuteTemplate(fqrn, pathAntPatterns, testData), + Check: resource.ComposeTestCheckFunc( + verifyXrayWatch(fqrn, testData), + resource.TestCheckResourceAttr(fqrn, "watch_resource.0.type", testData["watch_type"]), + resource.TestCheckResourceAttr(fqrn, "assigned_policy.0.name", testData["policy_name_0"]), + resource.TestCheckResourceAttr(fqrn, "assigned_policy.0.type", "security"), + resource.TestCheckResourceAttr(fqrn, "watch_resource.0.path_ant_filter.0.exclude_patterns.0", testData["exclude_patterns0"]), + resource.TestCheckResourceAttr(fqrn, "watch_resource.0.path_ant_filter.0.include_patterns.0", testData["include_patterns0"]), + resource.TestCheckResourceAttr(fqrn, "watch_resource.1.path_ant_filter.0.exclude_patterns.0", testData["exclude_patterns1"]), + resource.TestCheckResourceAttr(fqrn, "watch_resource.1.path_ant_filter.0.include_patterns.0", testData["include_patterns1"]), + ), + }, + }, + }) +} + +func TestAccWatch_PathAntPatternsError(t *testing.T) { + _, fqrn, resourceName := test.MkNames("watch-", "xray_watch") + testData := util.MergeMaps(testDataWatch) + + testData["resource_name"] = resourceName + testData["watch_name"] = fmt.Sprintf("xray-watch-%d", test.RandomInt()) + testData["policy_name_0"] = fmt.Sprintf("xray-policy-%d", test.RandomInt()) + testData["watch_type"] = "build" + testData["repo_type"] = "local" + testData["repo0"] = fmt.Sprintf("libs-release-local-0-%d", test.RandomInt()) + testData["repo1"] = fmt.Sprintf("libs-release-local-1-%d", test.RandomInt()) + testData["exclude_patterns0"] = "**/*.md" + testData["include_patterns0"] = "**/*.js" + testData["exclude_patterns1"] = "**/*.md" + testData["include_patterns1"] = "**/*.js" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { + testAccPreCheck(t) + testAccCreateRepos(t, testData["repo0"], "local", "") + testAccCreateRepos(t, testData["repo1"], "local", "") + }, + CheckDestroy: verifyDeleted(fqrn, func(id string, request *resty.Request) (*resty.Response, error) { + testAccDeleteRepo(t, testData["repo0"]) + testAccDeleteRepo(t, testData["repo1"]) + testCheckPolicyDeleted(testData["policy_name_0"], t, request) + resp, err := testCheckWatch(id, request) + return resp, err + }), + ProviderFactories: testAccProviders(), + Steps: []resource.TestStep{ + { + Config: util.ExecuteTemplate(fqrn, pathAntPatterns, testData), + ExpectError: regexp.MustCompile("attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to 'repository'"), + }, + }, + }) +} + func TestAccWatch_build(t *testing.T) { _, fqrn, resourceName := test.MkNames("watch-", "xray_watch") testData := util.MergeMaps(testDataWatch) @@ -829,6 +943,53 @@ resource "xray_watch" "{{ .resource_name }}" { watch_recipients = ["{{ .watch_recipient_0 }}", "{{ .watch_recipient_1 }}"] }` +const allReposPathAntFilterWatchTemplate = `resource "xray_security_policy" "security" { + name = "{{ .policy_name_0 }}" + description = "Security policy description" + type = "security" + rule { + name = "rule-name-severity" + priority = 1 + criteria { + min_severity = "High" + } + actions { + webhooks = [] + mails = ["test@email.com"] + block_download { + unscanned = true + active = true + } + block_release_bundle_distribution = true + fail_build = true + notify_watch_recipients = true + notify_deployer = true + create_ticket_enabled = false + build_failure_grace_period_in_days = 5 + } + } +} + +resource "xray_watch" "{{ .resource_name }}" { + name = "{{ .watch_name }}" + description = "{{ .description }}" + active = {{ .active }} + + watch_resource { + type = "{{ .watch_type }}" + path_ant_filter { + exclude_patterns = ["{{ .exclude_patterns0 }}"] + include_patterns = ["{{ .include_patterns0 }}"] + } + } + assigned_policy { + name = xray_security_policy.security.name + type = "security" + } + + watch_recipients = ["{{ .watch_recipient_0 }}", "{{ .watch_recipient_1 }}"] +}` + const allReposMultiplePoliciesWatchTemplate = `resource "xray_security_policy" "security" { name = "{{ .policy_name_0 }}" description = "Security policy description" @@ -1070,6 +1231,65 @@ resource "xray_watch" "{{ .resource_name }}" { watch_recipients = ["{{ .watch_recipient_0 }}", "{{ .watch_recipient_1 }}"] }` +const pathAntPatterns = `resource "xray_security_policy" "security" { + name = "{{ .policy_name_0 }}" + description = "Security policy description" + type = "security" + rule { + name = "rule-name-severity" + priority = 1 + criteria { + min_severity = "High" + } + actions { + webhooks = [] + mails = ["test@email.com"] + block_download { + unscanned = true + active = true + } + block_release_bundle_distribution = true + fail_build = true + notify_watch_recipients = true + notify_deployer = true + create_ticket_enabled = false + build_failure_grace_period_in_days = 5 + } + } +} + +resource "xray_watch" "{{ .resource_name }}" { + name = "{{ .watch_name }}" + description = "{{ .description }}" + active = {{ .active }} + + watch_resource { + type = "{{ .watch_type }}" + bin_mgr_id = "default" + name = "{{ .repo0 }}" + repo_type = "{{ .repo_type }}" + path_ant_filter { + exclude_patterns = ["{{ .exclude_patterns0 }}"] + include_patterns = ["{{ .include_patterns0 }}"] + } + } + watch_resource { + type = "{{ .watch_type }}" + bin_mgr_id = "default" + name = "{{ .repo1 }}" + repo_type = "{{ .repo_type }}" + path_ant_filter { + exclude_patterns = ["{{ .exclude_patterns1 }}"] + include_patterns = ["{{ .include_patterns1 }}"] + } +} + assigned_policy { + name = xray_security_policy.security.name + type = "security" +} + watch_recipients = ["{{ .watch_recipient_0 }}", "{{ .watch_recipient_1 }}"] +}` + const buildWatchTemplate = `resource "xray_security_policy" "security" { name = "{{ .policy_name_0 }}" description = "Security policy description" diff --git a/pkg/xray/watches.go b/pkg/xray/watches.go index 5105bb74..c2a74bb8 100644 --- a/pkg/xray/watches.go +++ b/pkg/xray/watches.go @@ -130,7 +130,12 @@ func unpackProjectResource(rawCfg interface{}) WatchProjectResource { } if v, ok := cfg["ant_filter"]; ok { - antFilters := unpackAntFilters(v.(*schema.Set)) + antFilters := unpackAntFilters(v.(*schema.Set), "ant-patterns") + resource.Filters = append(resource.Filters, antFilters...) + } + + if v, ok := cfg["path_ant_filter"]; ok { + antFilters := unpackAntFilters(v.(*schema.Set), "path-ant-patterns") resource.Filters = append(resource.Filters, antFilters...) } @@ -154,7 +159,7 @@ func unpackFilters(d *schema.Set) []WatchFilter { return filters } -func unpackAntFilters(d *schema.Set) []WatchFilter { +func unpackAntFilters(d *schema.Set, filterType string) []WatchFilter { tfFilters := d.List() var filters []WatchFilter @@ -173,7 +178,7 @@ func unpackAntFilters(d *schema.Set) []WatchFilter { ) filter := WatchFilter{ - Type: "ant-patterns", + Type: filterType, Value: json.RawMessage(filterJsonString), } filters = append(filters, filter) @@ -263,11 +268,16 @@ var packFilterMap = map[string]map[string]interface{}{ "func": packAntFilter, "attributeName": "ant_filter", }, + "path-ant-patterns": { + "func": packAntFilter, + "attributeName": "path_ant_filter", + }, } func packFilters(filters []WatchFilter, resources map[string]interface{}) (map[string]interface{}, []error) { resources["filter"] = []map[string]interface{}{} resources["ant_filter"] = []map[string]interface{}{} + resources["path_ant_filter"] = []map[string]interface{}{} var errors []error for _, filter := range filters { @@ -440,6 +450,11 @@ func watchResourceDiff(_ context.Context, diff *schema.ResourceDiff, v interface if !slices.Contains(antPatternsResourceTypes, resourceType) && len(antFilters) > 0 { return fmt.Errorf("attribute 'ant_filter' is set when 'watch_resource.type' is not set to 'all-builds' or 'all-projects'") } + pathAntFilters := r["path_ant_filter"].(*schema.Set).List() + pathAntPatternsResourceTypes := []string{"repository", "all-repos"} + if !slices.Contains(pathAntPatternsResourceTypes, resourceType) && len(pathAntFilters) > 0 { + return fmt.Errorf("attribute 'path_ant_filter' is set when 'watch_resource.type' is not set to 'repository' or 'all-repos'") + } } return nil }