diff --git a/Makefile b/Makefile index 3c95c83d..6879f88a 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ NAMESPACE=nobl9 NAME=nobl9 BIN_DIR=./bin BINARY=$(BIN_DIR)/terraform-provider-$(NAME) -VERSION=0.29.2 +VERSION=0.30.0 BUILD_FLAGS="-X github.com/nobl9/terraform-provider-nobl9/nobl9.Version=$(VERSION)" OS_ARCH?=linux_amd64 diff --git a/docs/index.md b/docs/index.md index 7b3eeb1a..5b7b0635 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,7 @@ The Nobl9 Provider delivers tools working with the Nobl9 API to create and manag - Alert Methods - Data Sources - Role Bindings +- Reports The Nobl9 Terraform Provider does not support the configuration of the following resources: - [SLO Annotations](https://docs.nobl9.com/features/slo-annotations/) diff --git a/docs/resources/report_system_health_review.md b/docs/resources/report_system_health_review.md new file mode 100644 index 00000000..19921401 --- /dev/null +++ b/docs/resources/report_system_health_review.md @@ -0,0 +1,189 @@ +--- +page_title: "nobl9_report_system_health_review Resource - terraform-provider-nobl9" +description: |- + System Health Review Report | Nobl9 Documentation https://docs.nobl9.com/reports/system-health-review/ +--- + +# nobl9_report_system_health_review (Resource) + +The System Health Review report facilitates recurring reliability check-ins by grouping your Nobl9 SLOs by projects or services and labels of your choice through the remaining error budget metric in a table-form report. + +## Example Usage + +Here's an example of Error Budget Status Report resource configuration: + +```terraform +resource "nobl9_report_system_health_review" "this" { + name = "my-shr-report" + display_name = "My System Health Review Report" + shared = true + row_group_by = "service" + + filters { + projects = ["project1", "project2"] + service { + name = "service1" + project = "project1" + } + service { + name = "service2" + project = "project2" + } + slo { + name = "my-slo" + project = "project1" + } + label { + key = "key1" + values = ["value1"] + } + } + + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "past" + date_time = "2024-09-05T09:58:37Z" + rrule = "FREQ=DAILY;INTERVAL=1" + } + } + + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + + column { + display_name = "Column 2" + label { + key = "key2" + values = ["value2"] + } + } + + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +``` + + +## Schema + +### Required + +- `column` (Block List, Min: 1) Columns to display in the report table. (see [below for nested schema](#nestedblock--column)) +- `name` (String) Unique name of the resource, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). +- `row_group_by` (String) Grouping methods of report table rows [project/service] +- `thresholds` (Block Set, Min: 1, Max: 1) Thresholds for Green, Yellow and Red statuses (e.g. healthy, at risk, exhausted budget). Yellow is calculated as the difference between Red and Green thresholds. If Red and Green are the same, Yellow is not used on the report. (see [below for nested schema](#nestedblock--thresholds)) +- `time_frame` (Block Set, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--time_frame)) + +### Optional + +- `display_name` (String) User-friendly display name of the resource. +- `filters` (Block List, Max: 1) Filters are used to select scope for Report. (see [below for nested schema](#nestedblock--filters)) +- `shared` (Boolean) Is report shared for all users with access to included projects. + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `column` + +Required: + +- `display_name` (String) Column display name. +- `label` (Block List, Min: 1) [Labels](https://docs.nobl9.com/features/labels/) containing a single key and a list of values. (see [below for nested schema](#nestedblock--column--label)) + + +### Nested Schema for `column.label` + +Required: + +- `key` (String) A key for the label, unique within the associated resource. +- `values` (List of String) A list of unique values for a single key. + + + + +### Nested Schema for `thresholds` + +Required: + +- `green_gt` (Number) Min value for the Green status (e.g. healthy). +- `red_lte` (Number) Max value for the Red status (e.g. exhausted budget). + +Optional: + +- `show_no_data` (Boolean) ShowNoData customizes the report to either show or hide rows with no data. + + + +### Nested Schema for `time_frame` + +Required: + +- `snapshot` (Block Set, Min: 1, Max: 1) (see [below for nested schema](#nestedblock--time_frame--snapshot)) +- `time_zone` (String) Timezone name in IANA Time Zone Database. + + +### Nested Schema for `time_frame.snapshot` + +Required: + +- `point` (String) The method of reporting time frame [past/latest] + +Optional: + +- `date_time` (String) Date and time of the past snapshot in RFC3339 format. +- `rrule` (String) The recurrence rule for the report past snapshot. The expected value is a string in RRULE format. Example: `FREQ=MONTHLY;BYMONTHDAY=1` + + + + +### Nested Schema for `filters` + +Optional: + +- `label` (Block List) [Labels](https://docs.nobl9.com/features/labels/) containing a single key and a list of values. (see [below for nested schema](#nestedblock--filters--label)) +- `projects` (List of String) Projects to pull data for report from. +- `service` (Block List) Services to pull data for report from. (see [below for nested schema](#nestedblock--filters--service)) +- `slo` (Block List) SLOs to pull data for report from. (see [below for nested schema](#nestedblock--filters--slo)) + + +### Nested Schema for `filters.label` + +Required: + +- `key` (String) A key for the label, unique within the associated resource. +- `values` (List of String) A list of unique values for a single key. + + + +### Nested Schema for `filters.service` + +Required: + +- `name` (String) Unique name of the resource, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). +- `project` (String) Name of the Nobl9 project the resource sits in, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + + + +### Nested Schema for `filters.slo` + +Required: + +- `name` (String) Unique name of the resource, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). +- `project` (String) Name of the Nobl9 project the resource sits in, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names). + +## Useful Links + +[Reports in Nobl9 | Nobl9 Documentation](https://docs.nobl9.com/reports/) + +[Reports YAML Configuration | Nobl9 Documentation](https://docs.nobl9.com/yaml-guide#report) \ No newline at end of file diff --git a/examples/resources/nobl9_report_system_health_review/resource.tf b/examples/resources/nobl9_report_system_health_review/resource.tf new file mode 100644 index 00000000..2049748c --- /dev/null +++ b/examples/resources/nobl9_report_system_health_review/resource.tf @@ -0,0 +1,58 @@ +resource "nobl9_report_system_health_review" "this" { + name = "my-shr-report" + display_name = "My System Health Review Report" + shared = true + row_group_by = "service" + + filters { + projects = ["project1", "project2"] + service { + name = "service1" + project = "project1" + } + service { + name = "service2" + project = "project2" + } + slo { + name = "my-slo" + project = "project1" + } + label { + key = "key1" + values = ["value1"] + } + } + + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "past" + date_time = "2024-09-05T09:58:37Z" + rrule = "FREQ=DAILY;INTERVAL=1" + } + } + + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + + column { + display_name = "Column 2" + label { + key = "key2" + values = ["value2"] + } + } + + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} + diff --git a/go.mod b/go.mod index a2285ff2..00088cf0 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637 github.com/hashicorp/terraform-plugin-docs v0.19.4 github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 - github.com/nobl9/nobl9-go v0.84.0 + github.com/nobl9/nobl9-go v0.84.1-rc6 github.com/stretchr/testify v1.9.0 github.com/teambition/rrule-go v1.8.2 ) @@ -24,7 +24,7 @@ require ( github.com/agext/levenshtein v1.2.3 // indirect github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/armon/go-radix v1.0.0 // indirect - github.com/aws/aws-sdk-go v1.54.17 // indirect + github.com/aws/aws-sdk-go v1.55.5 // indirect github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect github.com/cloudflare/circl v1.3.7 // indirect @@ -66,6 +66,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/nobl9/go-yaml v1.0.1 // indirect + github.com/nobl9/govy v0.1.1 // indirect github.com/oklog/run v1.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -79,15 +80,15 @@ require ( github.com/yuin/goldmark-meta v1.1.0 // indirect github.com/zclconf/go-cty v1.14.4 // indirect go.abhg.dev/goldmark/frontmatter v0.2.0 // indirect - golang.org/x/crypto v0.24.0 // indirect - golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 // indirect - golang.org/x/mod v0.18.0 // indirect - golang.org/x/net v0.26.0 // indirect - golang.org/x/sync v0.7.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.22.0 // indirect + golang.org/x/crypto v0.26.0 // indirect + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect + golang.org/x/mod v0.20.0 // indirect + golang.org/x/net v0.28.0 // indirect + golang.org/x/sync v0.8.0 // indirect + golang.org/x/sys v0.23.0 // indirect + golang.org/x/text v0.18.0 // indirect + golang.org/x/time v0.6.0 // indirect + golang.org/x/tools v0.24.0 // indirect golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect diff --git a/go.sum b/go.sum index a816b1a0..1386acf9 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,8 @@ github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= -github.com/aws/aws-sdk-go v1.54.17 h1:ZV/qwcCIhMHgsJ6iXXPVYI0s1MdLT+5LW28ClzCUPeI= -github.com/aws/aws-sdk-go v1.54.17/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU= +github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bmatcuk/doublestar/v4 v4.6.1 h1:FH9SifrbvJhnlQpztAx++wlkk70QBf0iBWDwNy7PA4I= @@ -178,8 +178,10 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/nobl9/go-yaml v1.0.1 h1:Aj1kSaYdRQTKlvS6ihvXzQJhCpoHhtf9nfA95zqWH4Q= github.com/nobl9/go-yaml v1.0.1/go.mod h1:t7vCO8ctYdBweZxU5lUgxzAw31+ZcqJYeqRtrv+5RHI= -github.com/nobl9/nobl9-go v0.84.0 h1:2mwwegUx/gAltbNmx4STq5A4iVahGVLZ7UI6QZepu0U= -github.com/nobl9/nobl9-go v0.84.0/go.mod h1:Jq840pSewkS1xvbwOENjqvNKugs3p3ZAdZG+iWZksGQ= +github.com/nobl9/govy v0.1.1 h1:l5UVwLuYhDhDFdmofH+SpE63Ta9IMTusFwY2MeJlCx4= +github.com/nobl9/govy v0.1.1/go.mod h1:MIhQelE3P6Ty7oqXef5ObzsxAo50+RiXmrywNy9ZRRw= +github.com/nobl9/nobl9-go v0.84.1-rc6 h1:19Z5yE5uR3aY9+u0pJTR4PvM9YaZWrbFn89wVwAKtC8= +github.com/nobl9/nobl9-go v0.84.1-rc6/go.mod h1:prAsgIuSyMUYbtQTh+o5prg8gykeEU3tfaU3gouOJBs= github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= @@ -234,25 +236,25 @@ go.abhg.dev/goldmark/frontmatter v0.2.0/go.mod h1:XqrEkZuM57djk7zrlRUB02x8I5J0px golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8 h1:LoYXNGAShUG3m/ehNk4iFctuhGX/+R1ZpfJ4/ia80JM= -golang.org/x/exp v0.0.0-20240604190554-fc45aab8b7f8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI= +golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= +golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= -golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= +golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= +golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -266,8 +268,8 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM= +golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -277,15 +279,15 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.22.0 h1:gqSGLZqv+AI9lIQzniJ0nZDRG5GBPsSi+DRNHWNz6yA= -golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= +golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= diff --git a/nobl9/helpers.go b/nobl9/helpers.go index 6369b0b1..12ffdb7b 100644 --- a/nobl9/helpers.go +++ b/nobl9/helpers.go @@ -24,7 +24,7 @@ func exactlyOneStringEmpty(str1, str2 string) bool { return (str1 == "" && str2 != "") || (str1 != "" && str2 == "") } -// oneElementSet implements schema.SchemaSetFunc and created only one element set. +// oneElementSet implements schema.SchemaSetFunc and creates only one element set. // Never use it for sets with more elements as new elements will override the old ones. func oneElementSet(_ interface{}) int { return 0 diff --git a/nobl9/provider.go b/nobl9/provider.go index a86e890f..c3462b03 100644 --- a/nobl9/provider.go +++ b/nobl9/provider.go @@ -110,6 +110,7 @@ func Provider() *schema.Provider { "nobl9_role_binding": resourceRoleBinding(), "nobl9_slo": resourceSLO(), "nobl9_budget_adjustment": budgetAdjustment(), + "nobl9_report_system_health_review": resourceReportFactory(reportSystemHealthReview{}), }, ConfigureContextFunc: providerConfigure, diff --git a/nobl9/resource_report.go b/nobl9/resource_report.go new file mode 100644 index 00000000..6c425d4f --- /dev/null +++ b/nobl9/resource_report.go @@ -0,0 +1,560 @@ +package nobl9 + +import ( + "context" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + "github.com/nobl9/nobl9-go/manifest" + v1alphaReport "github.com/nobl9/nobl9-go/manifest/v1alpha/report" + v1Objects "github.com/nobl9/nobl9-go/sdk/endpoints/objects/v1" +) + +type reportResource struct { + reportProvider +} + +type reportProvider interface { + GetSchema() map[string]*schema.Schema + GetDescription() string + MarshalSpec(spec v1alphaReport.Spec, resource resourceInterface) v1alphaReport.Spec + UnmarshalSpec(d *schema.ResourceData, spec v1alphaReport.Spec) diag.Diagnostics +} + +func resourceReportFactory(provider reportProvider) *schema.Resource { + i := reportResource{reportProvider: provider} + resource := &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": schemaName(), + "display_name": schemaDisplayName(), + "shared": schemaShared(), + "filters": schemaFilters(), + }, + CustomizeDiff: i.resourceReportValidate, + CreateContext: i.resourceReportApply, + UpdateContext: i.resourceReportApply, + DeleteContext: resourceReportDelete, + ReadContext: i.resourceReportRead, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Description: provider.GetDescription(), + } + + for k, v := range provider.GetSchema() { + resource.Schema[k] = v + } + + return resource +} + +func schemaShared() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeBool, + Optional: true, + Default: false, + Description: "Is report shared for all users with access to included projects.", + } +} + +func schemaFilters() *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Optional: true, + MinItems: 1, + MaxItems: 1, + Description: "Filters are used to select scope for Report.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "projects": { + Type: schema.TypeList, + Optional: true, + Description: "Projects to pull data for report from.", + Elem: &schema.Schema{ + Type: schema.TypeString, + Description: "Project name, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).", + }, + }, + "service": { + Type: schema.TypeList, + Optional: true, + Description: "Services to pull data for report from.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the resource, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).", + }, + "project": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Nobl9 project the resource sits in, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).", + }, + }, + }, + }, + "slo": { + Type: schema.TypeList, + Optional: true, + Description: "SLOs to pull data for report from.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Required: true, + Description: "Unique name of the resource, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).", + }, + "project": { + Type: schema.TypeString, + Required: true, + Description: "Name of the Nobl9 project the resource sits in, must conform to the naming convention from [DNS RFC1123](https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names).", + }, + }, + }, + }, + "label": schemaReportLabels(), + }, + }, + } +} + +func (r reportResource) marshalReport(ri resourceInterface) *v1alphaReport.Report { + spec := v1alphaReport.Spec{ + Shared: ri.Get("shared").(bool), + Filters: marshalReportFilters(ri.Get("filters")), + } + report := v1alphaReport.New( + v1alphaReport.Metadata{ + Name: ri.Get("name").(string), + DisplayName: ri.Get("display_name").(string), + }, + r.MarshalSpec(spec, ri), + ) + return &report +} + +func (r reportResource) unmarshalReport( + d *schema.ResourceData, + objects []v1alphaReport.Report, +) diag.Diagnostics { + if len(objects) != 1 { + d.SetId("") + return nil + } + object := objects[0] + var diags diag.Diagnostics + + diags = appendError(diags, d.Set("name", object.Metadata.Name)) + diags = appendError(diags, d.Set("display_name", object.Metadata.DisplayName)) + diags = appendError(diags, d.Set("shared", object.Spec.Shared)) + diags = appendError(diags, unmarshalReportFilters(d, object.Spec.Filters)) + + errs := r.UnmarshalSpec(d, object.Spec) + diags = append(diags, errs...) + return diags +} + +func marshalReportFilters(filtersRaw interface{}) *v1alphaReport.Filters { + if len(filtersRaw.([]interface{})) == 0 { + return nil + } + filters := filtersRaw.([]interface{})[0].(map[string]interface{}) + + projectList := filters["projects"].([]interface{}) + projects := make([]string, 0, len(projectList)) + for _, filter := range projectList { + projects = append(projects, filter.(string)) + } + + serviceList := filters["service"].([]interface{}) + services := make([]v1alphaReport.Service, 0, len(serviceList)) + for _, filter := range serviceList { + f := filter.(map[string]interface{}) + service := v1alphaReport.Service{ + Name: f["name"].(string), + Project: f["project"].(string), + } + services = append(services, service) + } + + sloList := filters["slo"].([]interface{}) + slos := make([]v1alphaReport.SLO, 0, len(sloList)) + for _, filter := range sloList { + f := filter.(map[string]interface{}) + slo := v1alphaReport.SLO{ + Name: f["name"].(string), + Project: f["project"].(string), + } + slos = append(slos, slo) + } + + return &v1alphaReport.Filters{ + Projects: projects, + Services: services, + SLOs: slos, + Labels: marshalReportLabels(filters["label"].([]interface{})), + } +} + +func unmarshalReportFilters(d *schema.ResourceData, filters *v1alphaReport.Filters) error { + services := make([]map[string]interface{}, 0, len(filters.Services)) + for _, service := range filters.Services { + serviceMap := map[string]interface{}{ + "name": service.Name, + "project": service.Project, + } + services = append(services, serviceMap) + } + + slos := make([]map[string]interface{}, 0, len(filters.SLOs)) + for _, slo := range filters.SLOs { + sloMap := map[string]interface{}{ + "name": slo.Name, + "project": slo.Project, + } + slos = append(slos, sloMap) + } + + f := map[string]interface{}{ + "projects": filters.Projects, + "service": services, + "slo": slos, + } + + if len(filters.Labels) > 0 { + f["label"] = unmarshalReportLabels(filters.Labels) + } + + return d.Set("filters", []interface{}{f}) +} + +func marshalReportLabels(labelList []interface{}) v1alphaReport.Labels { + labels, _ := marshalLabels(labelList) + reportLabels := make(map[v1alphaReport.LabelKey][]v1alphaReport.LabelValue, len(labels)) + for key, values := range labels { + reportLabels[key] = append(reportLabels[key], values...) + } + return reportLabels +} + +func unmarshalReportLabels(labelsRaw v1alphaReport.Labels) interface{} { + resultLabels := make([]map[string]interface{}, 0) + + for labelKey, labelValuesRaw := range labelsRaw { + var labelValuesStr []string + labelValuesStr = append(labelValuesStr, labelValuesRaw...) + labelKeyWithValues := make(map[string]interface{}) + labelKeyWithValues["key"] = labelKey + labelKeyWithValues["values"] = labelValuesStr + + resultLabels = append(resultLabels, labelKeyWithValues) + } + + return resultLabels +} + +func (r reportResource) resourceReportValidate(_ context.Context, d *schema.ResourceDiff, _ interface{}) error { + report := r.marshalReport(d) + errs := manifest.Validate([]manifest.Object{report}) + if errs != nil { + return formatErrorsAsSingleError(errs) + } + return nil +} + +func (r reportResource) resourceReportApply( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + report := r.marshalReport(d) + err := client.Objects().V1().Apply(ctx, []manifest.Object{report}) + if err != nil { + return diag.Errorf("could not add report: %s", err.Error()) + } + d.SetId(report.Metadata.Name) + return r.resourceReportRead(ctx, d, meta) +} + +func (r reportResource) resourceReportRead( + ctx context.Context, + d *schema.ResourceData, + meta interface{}, +) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + reports, err := client.Objects().V1().GetReports(ctx, v1Objects.GetReportsRequest{ + Names: []string{d.Id()}, + }) + if err != nil { + return diag.FromErr(err) + } + return r.unmarshalReport(d, reports) +} + +func resourceReportDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + err := client.Objects().V1().DeleteByName(ctx, manifest.KindReport, "", d.Id()) + if err != nil { + return diag.FromErr(err) + } + return nil +} + +type reportSystemHealthReview struct{} + +func (r reportSystemHealthReview) GetDescription() string { + return "[System Health Review Report | Nobl9 Documentation](https://docs.nobl9.com/reports/system-health-review/)" +} + +func (r reportSystemHealthReview) GetSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "time_frame": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "time_zone": { + Type: schema.TypeString, + Required: true, + Description: "Timezone name in IANA Time Zone Database.", + }, + "snapshot": { + Type: schema.TypeSet, + Required: true, + MinItems: 1, + MaxItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "point": { + Type: schema.TypeString, + Required: true, + Description: "The method of reporting time frame [past/latest]", + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice([]string{ + v1alphaReport.SnapshotPointPast.String(), + v1alphaReport.SnapshotPointLatest.String(), + }, false), + ), + }, + "date_time": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateDateTime, + Description: "Date and time of the past snapshot in RFC3339 format.", + }, + "rrule": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateRrule, + Description: "The recurrence rule for the report past snapshot. " + + "The expected value is a string in RRULE format. " + + "Example: `FREQ=MONTHLY;BYMONTHDAY=1`", + }, + }, + }, + }, + }, + }, + }, + "row_group_by": { + Type: schema.TypeString, + Required: true, + Description: "Grouping methods of report table rows [project/service]", + ValidateDiagFunc: validation.ToDiagFunc( + validation.StringInSlice([]string{ + v1alphaReport.RowGroupByProject.String(), + v1alphaReport.RowGroupByService.String(), + }, false), + ), + }, + "column": { + Type: schema.TypeList, + MinItems: 1, + Required: true, + Description: "Columns to display in the report table.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "display_name": { + Type: schema.TypeString, + Required: true, + Description: "Column display name.", + }, + "label": schemaColumnLabels(), + }, + }, + }, + "thresholds": { + Type: schema.TypeSet, + MinItems: 1, + MaxItems: 1, + Required: true, + Description: "Thresholds for Green, Yellow and Red statuses (e.g. healthy, at risk, exhausted budget). " + + "Yellow is calculated as the difference between Red and Green thresholds. " + + "If Red and Green are the same, Yellow is not used on the report.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "green_gt": { + Type: schema.TypeFloat, + Required: true, + Description: "Min value for the Green status (e.g. healthy).", + }, + "red_lte": { + Type: schema.TypeFloat, + Required: true, + Description: "Max value for the Red status (e.g. exhausted budget).", + }, + "show_no_data": { + Type: schema.TypeBool, + Optional: true, + Description: "ShowNoData customizes the report to either show or hide rows with no data.", + }, + }, + }, + }, + } +} + +func schemaReportLabels() *schema.Schema { + s := schemaLabels() + s.DiffSuppressFunc = nil + return s +} + +func schemaColumnLabels() *schema.Schema { + s := schemaReportLabels() + s.Optional = false + s.Required = true + s.MinItems = 1 + return s +} + +func (r reportSystemHealthReview) MarshalSpec(spec v1alphaReport.Spec, ri resourceInterface) v1alphaReport.Spec { + rowGroupBy, _ := v1alphaReport.ParseRowGroupBy(ri.Get("row_group_by").(string)) + spec.SystemHealthReview = &v1alphaReport.SystemHealthReviewConfig{ + TimeFrame: marshalReportTimeFrame(ri.Get("time_frame").(*schema.Set)), + RowGroupBy: rowGroupBy, + Columns: marshalReportColumns(ri.Get("column").([]interface{})), + Thresholds: marshalThresholds(ri.Get("thresholds").(*schema.Set)), + } + return spec +} + +func marshalReportTimeFrame(timeFrameSet *schema.Set) v1alphaReport.SystemHealthReviewTimeFrame { + if timeFrameSet.Len() == 0 { + return v1alphaReport.SystemHealthReviewTimeFrame{} + } + timeFrame := timeFrameSet.List()[0].(map[string]interface{}) + snapshotSet := timeFrame["snapshot"].(*schema.Set) + snapshotConfig := snapshotSet.List()[0].(map[string]interface{}) + var dateTime *time.Time + if snapshotConfig["date_time"] != nil { + if dt, err := time.Parse(time.RFC3339, snapshotConfig["date_time"].(string)); err == nil { + dateTime = &dt + } + } + point, _ := v1alphaReport.ParseSnapshotPoint(snapshotConfig["point"].(string)) + + snapshot := v1alphaReport.SnapshotTimeFrame{ + Point: point, + DateTime: dateTime, + } + if snapshotConfig["rrule"] != nil { + snapshot.Rrule = snapshotConfig["rrule"].(string) + } + return v1alphaReport.SystemHealthReviewTimeFrame{ + TimeZone: timeFrame["time_zone"].(string), + Snapshot: snapshot, + } +} + +func marshalThresholds(thresholdsSet *schema.Set) v1alphaReport.Thresholds { + if thresholdsSet.Len() == 0 { + return v1alphaReport.Thresholds{} + } + thresholds := thresholdsSet.List()[0].(map[string]interface{}) + redLte := thresholds["red_lte"].(float64) + greenGte := thresholds["green_gt"].(float64) + return v1alphaReport.Thresholds{ + RedLessThanOrEqual: &redLte, + GreenGreaterThan: &greenGte, + ShowNoData: thresholds["show_no_data"].(bool), + } +} + +func marshalReportColumns(columnsRaw []interface{}) []v1alphaReport.ColumnSpec { + columns := make([]v1alphaReport.ColumnSpec, 0, len(columnsRaw)) + for _, column := range columnsRaw { + c := column.(map[string]interface{}) + columns = append(columns, v1alphaReport.ColumnSpec{ + DisplayName: c["display_name"].(string), + Labels: marshalReportLabels(c["label"].([]interface{})), + }) + } + return columns +} + +func (r reportSystemHealthReview) UnmarshalSpec(d *schema.ResourceData, spec v1alphaReport.Spec) diag.Diagnostics { + config := spec.SystemHealthReview + var diags diag.Diagnostics + + diags = appendError(diags, d.Set("row_group_by", config.RowGroupBy.String())) + diags = appendError(diags, unmarshalReportTimeFrame(d, config.TimeFrame)) + diags = appendError(diags, unmarshalReportColumns(d, config.Columns)) + diags = appendError(diags, unmarshalReportThresholds(d, config.Thresholds)) + return diags +} + +func unmarshalReportTimeFrame(d *schema.ResourceData, timeFrame v1alphaReport.SystemHealthReviewTimeFrame) error { + snapshot := map[string]interface{}{ + "point": timeFrame.Snapshot.Point.String(), + "rrule": timeFrame.Snapshot.Rrule, + } + if timeFrame.Snapshot.DateTime != nil { + snapshot["date_time"] = timeFrame.Snapshot.DateTime.Format(time.RFC3339) + } + return d.Set("time_frame", schema.NewSet(oneElementSet, []interface{}{ + map[string]interface{}{ + "time_zone": timeFrame.TimeZone, + "snapshot": schema.NewSet(oneElementSet, []interface{}{snapshot}), + }, + })) +} + +func unmarshalReportColumns(d *schema.ResourceData, columns []v1alphaReport.ColumnSpec) error { + columnMap := make([]map[string]interface{}, 0, len(columns)) + for _, column := range columns { + columnMap = append(columnMap, map[string]interface{}{ + "display_name": column.DisplayName, + "label": unmarshalReportLabels(column.Labels), + }) + } + return d.Set("column", columnMap) +} + +func unmarshalReportThresholds(d *schema.ResourceData, thresholds v1alphaReport.Thresholds) error { + return d.Set("thresholds", schema.NewSet(oneElementSet, []interface{}{ + map[string]interface{}{ + "green_gt": thresholds.GreenGreaterThan, + "red_lte": thresholds.RedLessThanOrEqual, + "show_no_data": thresholds.ShowNoData, + }, + })) +} diff --git a/nobl9/resource_report_test.go b/nobl9/resource_report_test.go new file mode 100644 index 00000000..1bc5910c --- /dev/null +++ b/nobl9/resource_report_test.go @@ -0,0 +1,481 @@ +package nobl9 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/nobl9/nobl9-go/manifest" +) + +func TestAcc_Nobl9Reports(t *testing.T) { + cases := []struct { + name string + reportSuffix string + configFunc func(string) string + }{ + {"system-health-review-by-project-latest-snapshot", "system_health_review", testSHRLatestSnapshotByProject}, + {"system-health-review-by-service-latest-snapshot", "system_health_review", testSHRLatestSnapshot}, + {"system-health-review-by-service-past-snapshot-without-rrule", "system_health_review", testSHRPastSnapshot}, + { + "system-health-review-by-service-past-snapshot-with-rrule", + "system_health_review", + testSHRPastSnapshotWithRrule, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: ProviderFactory(), + CheckDestroy: CheckDestroy("nobl9_report_"+tc.reportSuffix, manifest.KindReport), + Steps: []resource.TestStep{ + { + Config: tc.configFunc(tc.name), + Check: CheckObjectCreated(fmt.Sprintf("nobl9_report_%s.%s", tc.reportSuffix, tc.name)), + }, + }, + }) + }) + } +} + +func TestAcc_Nobl9ReportsErrors(t *testing.T) { + t.Parallel() + cases := []struct { + name string + reportSuffix string + configFunc func(string) string + errorMessage string + }{ + {"system-health-review-wrong-grouping", + "system_health_review", + testSHRWrongGrouping, + `expected row_group_by to be one of`, + }, + {"system-health-review-empty-filters", + "system_health_review", + testSHREmptyFilters, + `property is required but was empty`, + }, + {"system-health-review-wrong-filters", + "system_health_review", + testSHRWrongFilters, + `at least one of the following fields is required: projects, services, slos`, + }, + {"system-health-review-empty-column", + "system_health_review", + testSHREmptyColumns, + `At least 1 "column" blocks are required`, + }, + {"system-health-review-columns-with-no-labels", + "system_health_review", + testSHRColumnsWithNoLabels, + `Insufficient label blocks`, + }, + {"system-health-review-empty-thresholds", + "system_health_review", + testSHREmptyThresholds, + `Insufficient thresholds blocks`, + }, + {"system-health-review-wrong-thresholds", + "system_health_review", + testSHRWrongThresholds, + `must be less than or equal to`, + }, + {"system-health-review-wrong-snapshot", + "system_health_review", + testSHRWrongSnapshot, + `property is required but was empty`, + }, + } + + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + resource.Test(t, resource.TestCase{ + ProviderFactories: ProviderFactory(), + CheckDestroy: CheckDestroy("nobl9_report_"+tc.reportSuffix, manifest.KindReport), + Steps: []resource.TestStep{ + { + Config: tc.configFunc(tc.name), + ExpectError: regexp.MustCompile(tc.errorMessage), + }, + }, + }) + }) + } +} + +func testSHRLatestSnapshotByProject(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRLatestSnapshot(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "service" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRPastSnapshot(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "service" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "past" + date_time = "2024-09-05T09:58:37Z" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRPastSnapshotWithRrule(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "service" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "past" + date_time = "2024-09-05T09:58:37Z" + rrule = "FREQ=DAILY;INTERVAL=1" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRWrongGrouping(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "wrong" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHREmptyFilters(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name) +} + +func testSHRWrongFilters(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + label { + key = "team" + values = ["green"] + } + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name) +} + +func testSHREmptyColumns(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRColumnsWithNoLabels(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHREmptyThresholds(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } +} +`, name, name, testProject) +} + +func testSHRWrongThresholds(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "latest" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.9 + green_gt = 0.8 + show_no_data = true + } +} +`, name, name, testProject) +} + +func testSHRWrongSnapshot(name string) string { + return fmt.Sprintf(` +resource "nobl9_report_system_health_review" "%s" { + name = "%s" + display_name = "System Health Review Report" + shared = true + row_group_by = "project" + filters { + projects = ["%s"] + } + time_frame { + time_zone = "Europe/Warsaw" + snapshot { + point = "past" + } + } + column { + display_name = "Column 1" + label { + key = "key1" + values = ["value1"] + } + } + thresholds { + red_lte = 0.8 + green_gt = 0.95 + show_no_data = true + } +} +`, name, name, testProject) +} diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 56a96a26..25970e8c 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -21,6 +21,7 @@ The Nobl9 Provider delivers tools working with the Nobl9 API to create and manag - Alert Methods - Data Sources - Role Bindings +- Reports The Nobl9 Terraform Provider does not support the configuration of the following resources: - [SLO Annotations](https://docs.nobl9.com/features/slo-annotations/) diff --git a/templates/resources/report_system_health_review.md.tmpl b/templates/resources/report_system_health_review.md.tmpl new file mode 100644 index 00000000..24340656 --- /dev/null +++ b/templates/resources/report_system_health_review.md.tmpl @@ -0,0 +1,23 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +The System Health Review report facilitates recurring reliability check-ins by grouping your Nobl9 SLOs by projects or services and labels of your choice through the remaining error budget metric in a table-form report. + +## Example Usage + +Here's an example of Error Budget Status Report resource configuration: + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} + +{{ .SchemaMarkdown | trimspace }} + +## Useful Links + +[Reports in Nobl9 | Nobl9 Documentation](https://docs.nobl9.com/reports/) + +[Reports YAML Configuration | Nobl9 Documentation](https://docs.nobl9.com/yaml-guide#report) \ No newline at end of file