From 41cde93f9697bb494906dc6a30a04e3c5d02dfab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Cegie=C5=82ka?= Date: Tue, 14 May 2024 11:02:36 +0200 Subject: [PATCH] feat: PC-11815 add budget adjustments terraform support (#175) ## Motivation Add support for the new `BudgetAdjustment` kind. ## Release Notes New BudgetAdjustment kind which allows customers to schedule recurring future events that should be excluded from the Error Budget calculations in their SLOs. This PR introduces new `budget_adjustment` resource. --------- Co-authored-by: kubaceg --- cspell.yaml | 2 + docs/resources/budget_adjustment.md | 96 ++++++ .../nobl9_budget_adjustment/resource.tf | 32 ++ go.mod | 2 +- nobl9/provider.go | 1 + nobl9/resource_budgetadjustment.go | 278 ++++++++++++++++++ nobl9/resource_budgetadjustment_test.go | 99 +++++++ nobl9/validate.go | 35 +++ nobl9/validate_test.go | 120 ++++++++ templates/resources/budget_adjustment.md.tmpl | 23 ++ 10 files changed, 687 insertions(+), 1 deletion(-) create mode 100644 docs/resources/budget_adjustment.md create mode 100644 examples/resources/nobl9_budget_adjustment/resource.tf create mode 100644 nobl9/resource_budgetadjustment.go create mode 100644 nobl9/resource_budgetadjustment_test.go create mode 100644 nobl9/validate.go create mode 100644 nobl9/validate_test.go create mode 100644 templates/resources/budget_adjustment.md.tmpl diff --git a/cspell.yaml b/cspell.yaml index 3e22713d..3d9ca388 100644 --- a/cspell.yaml +++ b/cspell.yaml @@ -26,6 +26,7 @@ ignorePaths: - bin/** words: - apdex + - bymonthday - dbslo - dynatrace - endef @@ -46,6 +47,7 @@ words: - opentsdb - plantuml - promql + - rrule - securego - sloctl - slos diff --git a/docs/resources/budget_adjustment.md b/docs/resources/budget_adjustment.md new file mode 100644 index 00000000..bb02e912 --- /dev/null +++ b/docs/resources/budget_adjustment.md @@ -0,0 +1,96 @@ +--- +page_title: "nobl9_budget_adjustment Resource - terraform-provider-nobl9" +description: |- + Budget adjustment configuration documentation https://docs.nobl9.com/features/budget-adjustment +--- + +# nobl9_budget_adjustment (Resource) + +The budget adjustment feature allows you to define future periods where planned maintenance, releases, and similar activities won't affect your budget in specific SLOs. + +For more details, refer to the [Budget adjustment configuration documentation](https://docs.nobl9.com/features/budget-adjustment). + +## Example Usage + +Here's an example of Budget Adjustment resource configuration: + +```terraform +resource "nobl9_budget_adjustment" "single-budget-adjustment-event" { + name = "single-budget-adjustment-event" + display_name = "Single Budget Adjustment Event" + first_event_start = "2022-01-01T00:00:00Z" + duration = "1h" + description = "Single budget adjustment event" + filters { + slos { + slo { + name = "my-slo" + project = "default" + } + } + } +} + +resource "nobl9_budget_adjustment" "recurring-budget-adjustment-event" { + name = "recurring-budget-adjustment-event" + display_name = "Recurring Budget Adjustment Event" + first_event_start = "2022-01-01T16:00:00Z" + duration = "1h" + rrule = "FREQ=WEEKLY" + description = "Recurring budget adjustment event" + filters { + slos { + slo { + name = "my-slo" + project = "default" + } + } + } +} +``` + + +## Schema + +### Required + +- `duration` (String) The duration of the budget adjustment event. The expected value for this field is a string formatted as a time duration. The duration must be defined with a precision of 1 minute. Example: `1h10m` +- `first_event_start` (String) The time at which the first event is scheduled to start. The expected value must be a string representing the date and time in RFC3339 format. Example: `2022-12-31T00:00:00Z` +- `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). + +### Optional + +- `description` (String) Optional description of the resource. Here, you can add details about who is responsible for the integration (team/owner) or the purpose of creating it. +- `display_name` (String) User-friendly display name of the resource. +- `filters` (Block Set) Filters are used to select SLOs for the budget adjustment event. (see [below for nested schema](#nestedblock--filters)) +- `rrule` (String) The recurrence rule for the budget adjustment event. The expected value is a string in RRULE format. Example: `FREQ=MONTHLY;BYMONTHDAY=1` + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `filters` + +Required: + +- `slos` (Block Set, Min: 1) (see [below for nested schema](#nestedblock--filters--slos)) + + +### Nested Schema for `filters.slos` + +Required: + +- `slo` (Block List, Min: 1) SLO where budget adjustment event will be applied. (see [below for nested schema](#nestedblock--filters--slos--slo)) + + +### Nested Schema for `filters.slos.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 + +[Budget Adjustment configuration | Nobl9 Documentation](https://docs.nobl9.com/yaml-guide#budget-adjustment) diff --git a/examples/resources/nobl9_budget_adjustment/resource.tf b/examples/resources/nobl9_budget_adjustment/resource.tf new file mode 100644 index 00000000..6628d698 --- /dev/null +++ b/examples/resources/nobl9_budget_adjustment/resource.tf @@ -0,0 +1,32 @@ +resource "nobl9_budget_adjustment" "single-budget-adjustment-event" { + name = "single-budget-adjustment-event" + display_name = "Single Budget Adjustment Event" + first_event_start = "2022-01-01T00:00:00Z" + duration = "1h" + description = "Single budget adjustment event" + filters { + slos { + slo { + name = "my-slo" + project = "default" + } + } + } +} + +resource "nobl9_budget_adjustment" "recurring-budget-adjustment-event" { + name = "recurring-budget-adjustment-event" + display_name = "Recurring Budget Adjustment Event" + first_event_start = "2022-01-01T16:00:00Z" + duration = "1h" + rrule = "FREQ=WEEKLY" + description = "Recurring budget adjustment event" + filters { + slos { + slo { + name = "my-slo" + project = "default" + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index f9104855..019fc5db 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/hashicorp/terraform-plugin-sdk/v2 v2.33.0 github.com/nobl9/nobl9-go v0.81.0 github.com/stretchr/testify v1.9.0 + github.com/teambition/rrule-go v1.8.2 ) require ( @@ -71,7 +72,6 @@ require ( github.com/posener/complete v1.2.3 // indirect github.com/shopspring/decimal v1.3.1 // indirect github.com/spf13/cast v1.5.0 // indirect - github.com/teambition/rrule-go v1.8.2 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect diff --git a/nobl9/provider.go b/nobl9/provider.go index 260c7ac0..3e28a050 100644 --- a/nobl9/provider.go +++ b/nobl9/provider.go @@ -109,6 +109,7 @@ func Provider() *schema.Provider { "nobl9_project": resourceProject(), "nobl9_role_binding": resourceRoleBinding(), "nobl9_slo": resourceSLO(), + "nobl9_budget_adjustment": budgetAdjustment(), }, ConfigureContextFunc: providerConfigure, diff --git a/nobl9/resource_budgetadjustment.go b/nobl9/resource_budgetadjustment.go new file mode 100644 index 00000000..7a70e2d6 --- /dev/null +++ b/nobl9/resource_budgetadjustment.go @@ -0,0 +1,278 @@ +package nobl9 + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/nobl9/nobl9-go/manifest" + "github.com/nobl9/nobl9-go/manifest/v1alpha/budgetadjustment" + v1 "github.com/nobl9/nobl9-go/sdk/endpoints/objects/v1" + "github.com/teambition/rrule-go" +) + +func budgetAdjustment() *schema.Resource { + return &schema.Resource{ + Schema: schemaBudgetAdjustment(), + CreateContext: resourceBudgetAdjustmentApply, + UpdateContext: resourceBudgetAdjustmentApply, + DeleteContext: resourceBudgetAdjustmentDelete, + ReadContext: resourceBudgetAdjustmentRead, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Description: "[Budget adjustment configuration documentation](https://docs.nobl9.com/features/budget-adjustment)", + } +} + +func schemaBudgetAdjustment() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "name": schemaName(), + "display_name": schemaDisplayName(), + "description": schemaDescription(), + "first_event_start": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDateTime, + Description: "The time at which the first event is scheduled to start. " + + "The expected value must be a string representing the date and time in RFC3339 format. " + + "Example: `2022-12-31T00:00:00Z`", + }, + "duration": { + Type: schema.TypeString, + Required: true, + ValidateDiagFunc: validateDuration, + Description: "The duration of the budget adjustment event. " + + "The expected value for this field is a string formatted as a time duration. " + + "The duration must be defined with a precision of 1 minute. " + + "Example: `1h10m`", + }, + "rrule": { + Type: schema.TypeString, + Optional: true, + ValidateDiagFunc: validateRrule, + Description: "The recurrence rule for the budget adjustment event. " + + "The expected value is a string in RRULE format. " + + "Example: `FREQ=MONTHLY;BYMONTHDAY=1`", + }, + "filters": { + Type: schema.TypeSet, + Optional: true, + Description: "Filters are used to select SLOs for the budget adjustment event.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "slos": { + Type: schema.TypeSet, + Required: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "slo": { + Type: schema.TypeList, + MinItems: 1, + Required: true, + Description: "SLO where budget adjustment event will be applied.", + 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).", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + } +} + +func validateDuration(v interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + _, err := time.ParseDuration(v.(string)) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid duration format", + Detail: fmt.Sprintf("Invalid duration format: %s", v), + AttributePath: path, + }) + } + return diags +} + +func validateRrule(v interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + _, err := rrule.StrToRRule(v.(string)) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid rrule format", + Detail: fmt.Sprintf("Invalid rrule format: %s", v), + AttributePath: path, + }) + } + return diags +} + +func resourceBudgetAdjustmentApply(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + + adjustment := marshalBudgetAdjustment(d) + + if err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutCreate)-time.Minute, func() *retry.RetryError { + err := client.Objects().V1().Apply(ctx, []manifest.Object{adjustment}) + if err != nil { + if errors.Is(err, errConcurrencyIssue) { + return retry.RetryableError(err) + } + return retry.NonRetryableError(err) + } + return nil + }); err != nil { + return diag.FromErr(err) + } + + d.SetId(adjustment.Metadata.Name) + return resourceBudgetAdjustmentRead(ctx, d, meta) +} + +func marshalBudgetAdjustment(d *schema.ResourceData) *budgetadjustment.BudgetAdjustment { + firstEventStart, _ := time.Parse(time.RFC3339, d.Get("first_event_start").(string)) + + adjustment := budgetadjustment.New( + budgetadjustment.Metadata{ + Name: d.Get("name").(string), + DisplayName: d.Get("display_name").(string), + }, + budgetadjustment.Spec{ + Description: d.Get("description").(string), + FirstEventStart: firstEventStart, + Duration: d.Get("duration").(string), + Rrule: d.Get("rrule").(string), + Filters: marshalFilters(d.Get("filters")), + }) + + return &adjustment +} + +func marshalFilters(filters interface{}) budgetadjustment.Filters { + filtersSet := filters.(*schema.Set) + if filtersSet.Len() == 0 { + return budgetadjustment.Filters{} + } + slos := filtersSet.List()[0].(map[string]interface{})["slos"].(*schema.Set) + slosList := slos.List()[0].(map[string]interface{})["slo"].([]interface{}) + sloRef := make([]budgetadjustment.SLORef, 0, len(slosList)) + for _, filter := range slosList { + f := filter.(map[string]interface{}) + slo := budgetadjustment.SLORef{ + Name: f["name"].(string), + Project: f["project"].(string), + } + sloRef = append(sloRef, slo) + } + + return budgetadjustment.Filters{ + SLOs: sloRef, + } +} + +func resourceBudgetAdjustmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + + budgetAdjustments, err := client.Objects().V1().GetBudgetAdjustments(ctx, v1.GetBudgetAdjustmentRequest{ + Names: []string{d.Id()}, + }) + if err != nil { + return diag.FromErr(err) + } + return unmarshalBudgetAdjustment(d, budgetAdjustments) +} + +func unmarshalBudgetAdjustment(d *schema.ResourceData, objects []budgetadjustment.BudgetAdjustment) diag.Diagnostics { + if len(objects) != 1 { + d.SetId("") + return nil + } + object := objects[0] + var diags diag.Diagnostics + var err error + + diags = appendError(diags, d.Set("name", object.Metadata.Name)) + diags = appendError(diags, d.Set("display_name", object.Metadata.DisplayName)) + diags = appendError(diags, d.Set("description", object.Spec.Description)) + diags = appendError(diags, d.Set("first_event_start", object.Spec.FirstEventStart.Format(time.RFC3339))) + diags = appendError(diags, d.Set("duration", object.Spec.Duration)) + diags = appendError(diags, d.Set("rrule", object.Spec.Rrule)) + + err = unmarshalFilters(d, object.Spec.Filters) + diags = appendError(diags, err) + + return diags +} + +func unmarshalFilters(d *schema.ResourceData, filters budgetadjustment.Filters) error { + 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{}{ + "slos": schema.NewSet(oneElementSet, []interface{}{ + map[string]interface{}{ + "slo": slos, + }, + }), + } + + return d.Set("filters", schema.NewSet(oneElementSet, []interface{}{f})) +} + +func resourceBudgetAdjustmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + config := meta.(ProviderConfig) + client, ds := getClient(config) + if ds != nil { + return ds + } + + if err := retry.RetryContext(ctx, d.Timeout(schema.TimeoutDelete)-time.Minute, func() *retry.RetryError { + err := client.Objects().V1().DeleteByName(ctx, manifest.KindBudgetAdjustment, "", d.Id()) + if err != nil { + if errors.Is(err, errConcurrencyIssue) { + return retry.RetryableError(err) + } + return retry.NonRetryableError(err) + } + return nil + }); err != nil { + return diag.FromErr(err) + } + + return nil +} diff --git a/nobl9/resource_budgetadjustment_test.go b/nobl9/resource_budgetadjustment_test.go new file mode 100644 index 00000000..6969a806 --- /dev/null +++ b/nobl9/resource_budgetadjustment_test.go @@ -0,0 +1,99 @@ +package nobl9 + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/nobl9/nobl9-go/manifest" +) + +func TestAcc_Nobl9BudgetAdjustments(t *testing.T) { + cases := []struct { + name string + configFunc func(string) string + }{ + {"single-event", testBudgetAdjustmentSingleEvent}, + {"recurring-event", testBudgetAdjustmentRecurringEvent}, + {"recurring-event-multiple-slos", testBudgetAdjustmentRecurringEventMultipleSlo}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + resource.ParallelTest(t, resource.TestCase{ + ProviderFactories: ProviderFactory(), + CheckDestroy: CheckDestroy("nobl9_budget_adjustment", manifest.KindBudgetAdjustment), + Steps: []resource.TestStep{ + { + Config: tc.configFunc(tc.name), + Check: CheckObjectCreated(fmt.Sprintf("nobl9_budget_adjustment.%s", tc.name)), + }, + }, + }) + }) + } +} + +func testBudgetAdjustmentSingleEvent(name string) string { + return fmt.Sprintf(` +resource "nobl9_budget_adjustment" "%s" { + name = "%s" + first_event_start = "2022-01-01T00:00:00Z" + duration = "1h" + filters { + slos { + slo { + name = "cloudwatch-ratio-slo" + project = "cloudwatch" + } + } + } +} +`, name, name) +} + +func testBudgetAdjustmentRecurringEvent(name string) string { + return fmt.Sprintf(`resource "nobl9_budget_adjustment" "%s" { + name = "%s" + first_event_start = "2022-01-01T00:00:00Z" + duration = "1h" + rrule = "FREQ=MONTHLY;BYMONTHDAY=1" + filters { + slos { + slo { + name = "cloudwatch-ratio-slo" + project = "cloudwatch" + } + slo { + name = "cloudwatch-ratio-slo2" + project = "cloudwatch" + } + } + } +}`, name, name) +} + +func testBudgetAdjustmentRecurringEventMultipleSlo(name string) string { + return fmt.Sprintf(` +resource "nobl9_budget_adjustment" "%s" { + name = "%s" + display_name = "Recurring budget adjustment for the first day of the month." + first_event_start = "2022-01-01T00:00:00Z" + description = "Recurring budget adjustment for the first day of the month." + duration = "1h" + rrule = "FREQ=MONTHLY;BYMONTHDAY=1" + filters { + slos { + slo { + name = "ratio-slo" + project = "default" + } + slo { + name = "ratio-slo-timeslices" + project = "default" + } + } + } +} +`, name, name) +} diff --git a/nobl9/validate.go b/nobl9/validate.go new file mode 100644 index 00000000..9415d98f --- /dev/null +++ b/nobl9/validate.go @@ -0,0 +1,35 @@ +package nobl9 + +import ( + "fmt" + "time" + + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +// validateDataTime validates the datetime format in RFC3339 +func validateDateTime(v interface{}, path cty.Path) diag.Diagnostics { + var diags diag.Diagnostics + + if _, ok := v.(string); !ok { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid type", + Detail: fmt.Sprintf("Expected string value got: %T", v), + AttributePath: path, + }) + return diags + } + + _, err := time.Parse(time.RFC3339, v.(string)) + if err != nil { + diags = append(diags, diag.Diagnostic{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: fmt.Sprintf("Invalid datetime format: %s", v), + AttributePath: path, + }) + } + return diags +} diff --git a/nobl9/validate_test.go b/nobl9/validate_test.go new file mode 100644 index 00000000..426b8634 --- /dev/null +++ b/nobl9/validate_test.go @@ -0,0 +1,120 @@ +package nobl9 + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +func TestValidateDateTime(t *testing.T) { + cases := []struct { + name string + in interface{} + expected diag.Diagnostics + }{ + { + name: "valid datetime", + in: "2021-09-26T07:00:00Z", + expected: nil, + }, + { + name: "invalid datetime", + in: "2021-09-26T07:00:00", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: 2021-09-26T07:00:00", + AttributePath: nil, + }}, + }, + { + name: "empty datetime", + in: "", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: ", + AttributePath: nil, + }}, + }, + { + name: "nil datetime", + in: nil, + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid type", + Detail: "Expected string value got: ", + AttributePath: nil, + }}, + }, + { + name: "invalid type", + in: 123, + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid type", + Detail: "Expected string value got: int", + AttributePath: nil, + }}, + }, + { + name: "invalid timezone", + in: "2021-09-26T07:00:00+01:00Z", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: 2021-09-26T07:00:00+01:00Z", + AttributePath: nil, + }}, + }, + { + name: "invalid date", + in: "2021-09-31T07:00:00Z", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: 2021-09-31T07:00:00Z", + AttributePath: nil, + }}, + }, + { + name: "invalid time", + in: "2021-09-26T24:00:00Z", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: 2021-09-26T24:00:00Z", + AttributePath: nil, + }}, + }, + { + name: "invalid seconds", + in: "2021-09-26T07:00:60Z", + expected: diag.Diagnostics{{ + Severity: diag.Error, + Summary: "Invalid datetime format", + Detail: "Invalid datetime format: 2021-09-26T07:00:60Z", + }}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := validateDateTime(tc.in, nil) + if len(tc.expected) != len(actual) { + t.Errorf("expected %d diagnostics, got %d", len(tc.expected), len(actual)) + } + for i, d := range actual { + if d.Severity != tc.expected[i].Severity { + t.Errorf("expected severity %d, got %d", d.Severity, tc.expected[i].Severity) + } + if d.Summary != tc.expected[i].Summary { + t.Errorf("expected summary %s, got %s", d.Summary, tc.expected[i].Summary) + } + if d.Detail != tc.expected[i].Detail { + t.Errorf("expected detail %s, got %s", d.Detail, tc.expected[i].Detail) + } + } + }) + } +} diff --git a/templates/resources/budget_adjustment.md.tmpl b/templates/resources/budget_adjustment.md.tmpl new file mode 100644 index 00000000..15497c87 --- /dev/null +++ b/templates/resources/budget_adjustment.md.tmpl @@ -0,0 +1,23 @@ +--- +page_title: "{{.Name}} {{.Type}} - {{.ProviderName}}" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Name}} ({{.Type}}) + +The budget adjustment feature allows you to define future periods where planned maintenance, releases, and similar activities won't affect your budget in specific SLOs. + +For more details, refer to the {{ .Description | trimspace }}. + +## Example Usage + +Here's an example of Budget Adjustment resource configuration: + +{{ tffile (printf "examples/resources/%s/resource.tf" .Name)}} + +{{ .SchemaMarkdown | trimspace }} + +## Useful Links + +[Budget Adjustment configuration | Nobl9 Documentation](https://docs.nobl9.com/yaml-guide#budget-adjustment)