From 6fccc029f560827e08802381f37c9371885ce30a Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Tue, 18 Mar 2025 20:03:15 +0530 Subject: [PATCH 01/11] Add resource turbot_campaign. Closes #195 --- CHANGELOG.md | 6 + apiClient/campaign.go | 39 ++++ apiClient/queries.go | 100 +++++++++ apiClient/types.go | 40 ++++ errors/errors.go | 9 +- turbot/provider.go | 1 + turbot/resource_turbot_campaign.go | 282 ++++++++++++++++++++++++ turbot/resource_turbot_campaign_test.go | 120 ++++++++++ 8 files changed, 596 insertions(+), 1 deletion(-) create mode 100644 apiClient/campaign.go create mode 100644 turbot/resource_turbot_campaign.go create mode 100644 turbot/resource_turbot_campaign_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 6647756c..bcda4893 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## 1.13.0 (Unreleased) + +FEATURES: + +* **New Resource:** `turbot_campaign` ([#195](https://github.com/turbot/terraform-provider-turbot/issues/195)) + ## 1.12.0 (February 19, 2025) FEATURES: diff --git a/apiClient/campaign.go b/apiClient/campaign.go new file mode 100644 index 00000000..88ed687e --- /dev/null +++ b/apiClient/campaign.go @@ -0,0 +1,39 @@ +package apiClient + +func (client *Client) CreateCampaign(input map[string]interface{}) (*Campaign, error) { + query := createCampaignMutation() + responseData := &CampaignResponse{} + variables := map[string]interface{}{ + "input": input, + } + + // execute api call + if err := client.doRequest(query, variables, responseData); err != nil { + return nil, client.handleCreateError(err, input, "campaign") + } + return &responseData.Campaign, nil +} + +func (client *Client) ReadCampaign(id string) (*Campaign, error) { + query := readCampaignQuery(id) + responseData := &CampaignResponse{} + + // execute api call + if err := client.doRequest(query, nil, responseData); err != nil { + return nil, client.handleReadError(err, id, "campaign") + } + return &responseData.Campaign, nil +} + +func (client *Client) UpdateCampaign(input map[string]interface{}) (*Campaign, error) { + query := updateCampaignMutation() + responseData := &CampaignResponse{} + variables := map[string]interface{}{ + "input": input, + } + // execute api call + if err := client.doRequest(query, variables, responseData); err != nil { + return nil, client.handleUpdateError(err, input, "campaign") + } + return &responseData.Campaign, nil +} diff --git a/apiClient/queries.go b/apiClient/queries.go index 5164010a..1b065d3d 100644 --- a/apiClient/queries.go +++ b/apiClient/queries.go @@ -732,3 +732,103 @@ func (client *Client) GetTurbotWorkspaceVersion() (*semver.Version, error) { } return version, nil } + +// campaign +// filter and description are removed for a workaround, will be removed after a Core change. +func createCampaignMutation() string { + return `mutation CreateCampaign($input: CreateCampaignInput!) { + campaign: createCampaign(input: $input) { + title + status + description + recipients + phases + turbot { + id + parentId + akas + title + } + accounts { + items { + turbot { + id + } + } + } + guardrails { + items { + turbot { + id + } + } + } + } + }` +} + +func readCampaignQuery(id string) string { + return fmt.Sprintf(`{ + campaign(id: "%s") { + description + status + title + parent { + turbot { + id + } + } + recipients + phases + accounts { + items { + turbot { + id + } + } + } + guardrails { + items { + turbot { + id + } + } + } + turbot { + id + } + } + }`, id) +} + +func updateCampaignMutation() string { + return `mutation UpdateCampaign($input: UpdateCampaignInput!) { + campaign: updateCampaign(input: $input) { + title + status + description + recipients + phases + turbot { + id + parentId + akas + title + } + accounts { + items { + turbot { + id + } + } + } + guardrails { + items { + turbot { + id + } + } + } + } + }` +} diff --git a/apiClient/types.go b/apiClient/types.go index 285ade65..4ddb1f56 100644 --- a/apiClient/types.go +++ b/apiClient/types.go @@ -520,3 +520,43 @@ type TurbotWatchMetadata struct { ResourceId string FavoriteId string } + +// Campaign +type CampaignResponse struct { + Campaign Campaign +} + +type Campaign struct { + Turbot TurbotResourceMetadata + Description string + Status string + Title string + Recipients []string + Accounts struct { + Items []struct { + Turbot TurbotResourceMetadata + } + } + Guardrails struct { + Items []struct { + Turbot TurbotResourceMetadata + } + } + Phases CampaignPhases `json:"phases"` +} + +type CampaignPhases struct { + Draft *TurbotCampaignPhaseMetadata `json:"draft,omitempty"` + Preview *TurbotCampaignPhaseMetadata `json:"preview,omitempty"` + Check *TurbotCampaignPhaseMetadata `json:"check,omitempty"` + Enforce *TurbotCampaignPhaseMetadata `json:"enforce,omitempty"` + Detach *TurbotCampaignPhaseMetadata `json:"detach,omitempty"` +} + +type TurbotCampaignPhaseMetadata struct { + TransitionAt string `json:"transitionAt,omitempty"` + TransitionNotice string `json:"transitionNotice,omitempty"` + TransitionWhen string `json:"transitionWhen,omitempty"` + WarnAt []string `json:"warnAt,omitempty"` + Recipients []string `json:"recipients,omitempty"` +} diff --git a/errors/errors.go b/errors/errors.go index 2b630f7b..bce809a3 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -2,11 +2,12 @@ package errors import ( "fmt" - "github.com/pkg/errors" "net/http" "regexp" "strconv" "strings" + + "github.com/pkg/errors" ) func NotFoundError(err error) bool { @@ -21,6 +22,12 @@ func FailedValidationError(err error) bool { return expectedErr.Match([]byte(err.Error())) } +func ForbiddenError(err error) bool { + forbiddenErr := "graphql: Forbidden: Insufficient permissions for resource(?i)" + expectedErr := regexp.MustCompile(forbiddenErr) + return expectedErr.Match([]byte(err.Error())) +} + func ExtractErrorCode(err error) (int, error) { // error returned from machinebox/graphql is of graphql type // errorNon200Template = "graphql: server returned a non-200 status code: 503" diff --git a/turbot/provider.go b/turbot/provider.go index 9754e02b..364ca44d 100755 --- a/turbot/provider.go +++ b/turbot/provider.go @@ -35,6 +35,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ + "turbot_campaign": resourceTurbotCampaign(), "turbot_control_mute": resourceTurbotControlMute(), "turbot_file": resourceTurbotFile(), "turbot_folder": resourceTurbotFolder(), diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go new file mode 100644 index 00000000..dfe0a197 --- /dev/null +++ b/turbot/resource_turbot_campaign.go @@ -0,0 +1,282 @@ +package turbot + +import ( + "reflect" + + "github.com/hashicorp/terraform/helper/schema" + "github.com/turbot/terraform-provider-turbot/apiClient" + "github.com/turbot/terraform-provider-turbot/errors" + "github.com/turbot/terraform-provider-turbot/helpers" +) + +// properties which must be passed to a create/update call +var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "guardrails", "accounts"} + +func getCampaignUpdateProperties() []interface{} { + excludedProperties := []string{"guardrails", "preview", "check", "draft", "enforce", "akas"} + return helpers.RemoveProperties(campaignProperties, excludedProperties) +} + +func resourceTurbotCampaign() *schema.Resource { + return &schema.Resource{ + Create: resourceTurbotCampaignCreate, + Read: resourceTurbotCampaignRead, + Update: resourceTurbotCampaignUpdate, + Delete: resourceTurbotCampaignDelete, + Exists: resourceTurbotCampaignExists, + Importer: &schema.ResourceImporter{ + State: resourceTurbotCampaignImport, + }, + Schema: map[string]*schema.Schema{ + "guardrails": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "accounts": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "recipients": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + "title": { + Type: schema.TypeString, + Optional: true, + }, + "description": { + Type: schema.TypeString, + Optional: true, + }, + "status": { + Type: schema.TypeString, + Optional: true, + }, + "parent": { + Type: schema.TypeString, + Computed: true, + }, + "check": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: phaseSchema(), + }, + }, + "detach": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: phaseSchema(), + }, + }, + "draft": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: phaseSchema(), + }, + }, + "enforce": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: phaseSchema(), + }, + }, + "preview": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Resource{ + Schema: phaseSchema(), + }, + }, + "akas": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { + // Suppress the diff, since the akas cannot be updated after creation + return true + }, + }, + }, + } +} + +func phaseSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "transition_at": { + Type: schema.TypeString, + Required: true, + }, + "transition_notice": { + Type: schema.TypeString, + Optional: true, + }, + "transition_when": { + Type: schema.TypeString, + Optional: true, + }, + "warn_at": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } +} + +func resourceTurbotCampaignExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { + client := meta.(*apiClient.Client) + id := d.Id() + return client.ResourceExists(id) +} + +func resourceTurbotCampaignCreate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*apiClient.Client) + + // build map of folder properties + input := mapFromResourceData(d, campaignProperties) + + phases := map[string]interface{}{} + if !isNil(input["preview"]) { + phases["preview"] = setPhaseAttribute(input, "preview") + delete(input, "preview") + } + + if !isNil(input["check"]) { + phases["check"] = setPhaseAttribute(input, "check") + delete(input, "check") + } + input["phases"] = phases + + // panic(fmt.Sprintf("HERE >>> %+v", input)) + + campaign, err := client.CreateCampaign(input) + if err != nil { + return err + } + + // assign the id + d.SetId(campaign.Turbot.Id) + + // TODO Remove Read call once schema changes are In. + return resourceTurbotCampaignRead(d, meta) +} + +func resourceTurbotCampaignUpdate(d *schema.ResourceData, meta interface{}) error { + client := meta.(*apiClient.Client) + id := d.Id() + + // build map of folder properties + input := mapFromResourceData(d, getCampaignUpdateProperties()) + input["id"] = id + + _, err := client.UpdateCampaign(input) + if err != nil { + return err + } + // set 'Read' Properties + return resourceTurbotCampaignRead(d, meta) +} + +func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error { + client := meta.(*apiClient.Client) + id := d.Id() + + campaign, err := client.ReadCampaign(id) + if err != nil { + if errors.NotFoundError(err) { + // folder was not found - clear id + d.SetId("") + } + return err + } + + d.Set("description", campaign.Description) + d.Set("status", campaign.Status) + d.Set("title", campaign.Title) + d.Set("recipients", campaign.Recipients) + d.Set("akas", campaign.Turbot.Akas) + + if len(campaign.Accounts.Items) > 0 { + accounts := []string{} + for _, item := range campaign.Accounts.Items { + accounts = append(accounts, item.Turbot.Id) + } + d.Set("accounts", accounts) + } + if len(campaign.Guardrails.Items) > 0 { + guardrails := []string{} + for _, item := range campaign.Guardrails.Items { + guardrails = append(guardrails, item.Turbot.Id) + } + d.Set("guardrails", guardrails) + } + if !isNil(campaign.Turbot.ParentId) { + d.Set("parent", campaign.Turbot.ParentId) + } + + d.Set("preview", []interface{}{campaign.Phases.Preview}) + d.Set("check", []interface{}{campaign.Phases.Check}) + + return nil +} + +func resourceTurbotCampaignDelete(d *schema.ResourceData, meta interface{}) error { + client := meta.(*apiClient.Client) + id := d.Id() + err := client.DeleteResource(id) + if err != nil { + return err + } + + // clear the id to show we have deleted + d.SetId("") + + return nil +} + +func resourceTurbotCampaignImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if err := resourceTurbotCampaignRead(d, meta); err != nil { + return nil, err + } + return []*schema.ResourceData{d}, nil +} + +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} + +func setPhaseAttribute(input map[string]interface{}, attributeName string) map[string]interface{} { + phaseList := input[attributeName].([]interface{}) + if len(phaseList) > 0 { + phase := phaseList[0].(map[string]interface{}) + + return map[string]interface{}{ + "transitionAt": phase["transition_at"].(string), + "transitionNotice": phase["transition_notice"].(string), + "transitionWhen": phase["transition_when"].(string), + "warnAt": phase["warn_at"].([]interface{}), + } + } + return nil +} diff --git a/turbot/resource_turbot_campaign_test.go b/turbot/resource_turbot_campaign_test.go new file mode 100644 index 00000000..8ccbfbeb --- /dev/null +++ b/turbot/resource_turbot_campaign_test.go @@ -0,0 +1,120 @@ +package turbot + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform/helper/resource" + "github.com/hashicorp/terraform/terraform" + "github.com/turbot/terraform-provider-turbot/apiClient" + "github.com/turbot/terraform-provider-turbot/errors" +) + +// test suites +func TestAccCampaign_Basic(t *testing.T) { + resourceName := "turbot_campaign.test" + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckCampaignDestroy, + Steps: []resource.TestStep{ + { + Config: testAccCampaignConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckCampaignExists("turbot_campaign.test"), + resource.TestCheckResourceAttr("turbot_campaign.test", "title", "Test Campaign Resource Created Through Terraform"), + resource.TestCheckResourceAttr("turbot_campaign.test", "description", "Campaign For Testing"), + ), + }, + { + Config: testAccCampaignUpdateDescConfig(), + Check: resource.ComposeTestCheckFunc( + testAccCheckCampaignExists("turbot_campaign.test"), + resource.TestCheckResourceAttr("turbot_campaign.test", "title", "Test Campaign Resource Created Through Terraform"), + resource.TestCheckResourceAttr("turbot_campaign.test", "description", "Campaign For Testing Updated"), + ), + }, + { + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) + +} + +// configs +func testAccCampaignConfig() string { + return ` +resource "turbot_campaign" "test" { + + title = "Test Campaign Resource Created Through Terraform" + description = "Campaign For Testing" + + guardrails = ["348351678444334"] + accounts = ["330101957430965"] + + preview { + transition_at = "2025-11-29T00:00:00Z" + transition_when = "no_alerts" + transition_notice = "enabled" + } +} +` +} + +func testAccCampaignUpdateDescConfig() string { + return ` +resource "turbot_campaign" "test" { + + title = "Test Campaign Resource Created Through Terraform" + description = "Campaign For Testing Updated" + + guardrails = ["348351678444334"] + accounts = ["330101957430965"] + + preview { + transition_at = "2025-11-29T00:00:00Z" + transition_when = "no_alerts" + transition_notice = "enabled" + } +} +` +} + +// helper functions +func testAccCheckCampaignExists(resource string) resource.TestCheckFunc { + return func(state *terraform.State) error { + rs, ok := state.RootModule().Resources[resource] + if !ok { + return fmt.Errorf("not found: %s", resource) + } + if rs.Primary.ID == "" { + return fmt.Errorf("no Record ID is set") + } + client := testAccProvider.Meta().(*apiClient.Client) + _, err := client.ReadCampaign(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error fetching item with resource %s. %s", resource, err) + } + return nil + } +} + +func testAccCheckCampaignDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*apiClient.Client) + for _, rs := range s.RootModule().Resources { + if rs.Type == "turbot_campaign" { + _, err := client.ReadCampaign(rs.Primary.ID) + if err == nil { + return fmt.Errorf("alert still exists") + } + if !errors.ForbiddenError(err) { + return fmt.Errorf("expected 'forbidden' error, got %s", err) + } + } + } + + return nil +} From 0c41f1ca1144316268cabd38b914b6003f829ab2 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Wed, 19 Mar 2025 17:23:57 +0530 Subject: [PATCH 02/11] Fixed create operation to configure akas --- apiClient/queries.go | 32 ++++++++++++++---------------- turbot/resource_turbot_campaign.go | 9 +++------ 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/apiClient/queries.go b/apiClient/queries.go index 1b065d3d..0c2fc7eb 100644 --- a/apiClient/queries.go +++ b/apiClient/queries.go @@ -773,11 +773,6 @@ func readCampaignQuery(id string) string { description status title - parent { - turbot { - id - } - } recipients phases accounts { @@ -796,6 +791,9 @@ func readCampaignQuery(id string) string { } turbot { id + akas + title + parentId } } }`, id) @@ -810,25 +808,25 @@ func updateCampaignMutation() string { recipients phases turbot { - id - parentId - akas - title + id + parentId + akas + title } accounts { - items { - turbot { - id + items { + turbot { + id + } } } - } guardrails { - items { - turbot { - id + items { + turbot { + id + } } } - } } }` } diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index dfe0a197..c227d3cb 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -10,7 +10,7 @@ import ( ) // properties which must be passed to a create/update call -var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "guardrails", "accounts"} +var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "guardrails", "accounts", "akas"} func getCampaignUpdateProperties() []interface{} { excludedProperties := []string{"guardrails", "preview", "check", "draft", "enforce", "akas"} @@ -106,10 +106,7 @@ func resourceTurbotCampaign() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, - DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { - // Suppress the diff, since the akas cannot be updated after creation - return true - }, + DiffSuppressFunc: suppressIfAkaRemoved(), }, }, } @@ -206,7 +203,7 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error d.Set("description", campaign.Description) d.Set("status", campaign.Status) - d.Set("title", campaign.Title) + d.Set("title", campaign.Turbot.Title) d.Set("recipients", campaign.Recipients) d.Set("akas", campaign.Turbot.Akas) From b574d765866ee092c1aacd0abbe3ae5c198c57d3 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Wed, 2 Apr 2025 11:59:29 +0530 Subject: [PATCH 03/11] Rename phase attributes --- turbot/resource_turbot_campaign.go | 12 ++++++------ turbot/resource_turbot_campaign_test.go | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index c227d3cb..214b97eb 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -114,15 +114,15 @@ func resourceTurbotCampaign() *schema.Resource { func phaseSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ - "transition_at": { + "start_at": { Type: schema.TypeString, Required: true, }, - "transition_notice": { + "start_notice": { Type: schema.TypeString, Optional: true, }, - "transition_when": { + "start_early_if": { Type: schema.TypeString, Optional: true, }, @@ -269,9 +269,9 @@ func setPhaseAttribute(input map[string]interface{}, attributeName string) map[s phase := phaseList[0].(map[string]interface{}) return map[string]interface{}{ - "transitionAt": phase["transition_at"].(string), - "transitionNotice": phase["transition_notice"].(string), - "transitionWhen": phase["transition_when"].(string), + "transitionAt": phase["start_at"].(string), + "transitionNotice": phase["start_notice"].(string), + "transitionWhen": phase["start_early_if"].(string), "warnAt": phase["warn_at"].([]interface{}), } } diff --git a/turbot/resource_turbot_campaign_test.go b/turbot/resource_turbot_campaign_test.go index 8ccbfbeb..170eab5a 100644 --- a/turbot/resource_turbot_campaign_test.go +++ b/turbot/resource_turbot_campaign_test.go @@ -56,9 +56,9 @@ resource "turbot_campaign" "test" { accounts = ["330101957430965"] preview { - transition_at = "2025-11-29T00:00:00Z" - transition_when = "no_alerts" - transition_notice = "enabled" + start_at = "2025-11-29T00:00:00Z" + start_early_if = "no_alerts" + start_notice = "enabled" } } ` @@ -75,9 +75,9 @@ resource "turbot_campaign" "test" { accounts = ["330101957430965"] preview { - transition_at = "2025-11-29T00:00:00Z" - transition_when = "no_alerts" - transition_notice = "enabled" + start_at = "2025-11-29T00:00:00Z" + start_early_if = "no_alerts" + start_notice = "enabled" } } ` From 74c8b76b62c463c9f60a42e3ea0a9512117b0e47 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Thu, 3 Apr 2025 15:46:22 +0530 Subject: [PATCH 04/11] Update phase attributes --- apiClient/types.go | 24 ++++---- turbot/resource_data_helpers.go | 14 +++++ turbot/resource_turbot_campaign.go | 96 +++++++++++++++++++++--------- 3 files changed, 95 insertions(+), 39 deletions(-) diff --git a/apiClient/types.go b/apiClient/types.go index 4ddb1f56..1713eca7 100644 --- a/apiClient/types.go +++ b/apiClient/types.go @@ -546,17 +546,21 @@ type Campaign struct { } type CampaignPhases struct { - Draft *TurbotCampaignPhaseMetadata `json:"draft,omitempty"` - Preview *TurbotCampaignPhaseMetadata `json:"preview,omitempty"` - Check *TurbotCampaignPhaseMetadata `json:"check,omitempty"` - Enforce *TurbotCampaignPhaseMetadata `json:"enforce,omitempty"` - Detach *TurbotCampaignPhaseMetadata `json:"detach,omitempty"` + Draft *TurbotCampaignDraftPhaseMetadata `json:"draft,omitempty"` + Preview *TurbotCampaignPhaseMetadata `json:"preview,omitempty"` + Check *TurbotCampaignPhaseMetadata `json:"check,omitempty"` + Enforce *TurbotCampaignPhaseMetadata `json:"enforce,omitempty"` + Detach *TurbotCampaignPhaseMetadata `json:"detach,omitempty"` } type TurbotCampaignPhaseMetadata struct { - TransitionAt string `json:"transitionAt,omitempty"` - TransitionNotice string `json:"transitionNotice,omitempty"` - TransitionWhen string `json:"transitionWhen,omitempty"` - WarnAt []string `json:"warnAt,omitempty"` - Recipients []string `json:"recipients,omitempty"` + StartAt string `json:"startAt,omitempty"` + StartNotice string `json:"startNotice,omitempty"` + StartEarlyIf string `json:"startEarlyIf,omitempty"` + WarnAt []string `json:"warnAt,omitempty"` + Recipients []string `json:"recipients,omitempty"` +} + +type TurbotCampaignDraftPhaseMetadata struct { + StartAt string `json:"startAt,omitempty"` } diff --git a/turbot/resource_data_helpers.go b/turbot/resource_data_helpers.go index 8e3c165c..6c331345 100644 --- a/turbot/resource_data_helpers.go +++ b/turbot/resource_data_helpers.go @@ -1,6 +1,8 @@ package turbot import ( + "reflect" + "github.com/hashicorp/terraform/helper/schema" "github.com/iancoleman/strcase" "github.com/turbot/terraform-provider-turbot/apiClient" @@ -58,3 +60,15 @@ func storeAkas(aka, propertyName string, d *schema.ResourceData, meta interface{ d.Set(propertyName, akas) return nil } + +// given an attribute, check for if the attribute contains a nil value +func isNil(i interface{}) bool { + if i == nil { + return true + } + switch reflect.TypeOf(i).Kind() { + case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice: + return reflect.ValueOf(i).IsNil() + } + return false +} diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index 214b97eb..dfc5fdd6 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -1,8 +1,6 @@ package turbot import ( - "reflect" - "github.com/hashicorp/terraform/helper/schema" "github.com/turbot/terraform-provider-turbot/apiClient" "github.com/turbot/terraform-provider-turbot/errors" @@ -10,10 +8,10 @@ import ( ) // properties which must be passed to a create/update call -var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "guardrails", "accounts", "akas"} +var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "detach", "guardrails", "accounts", "akas"} func getCampaignUpdateProperties() []interface{} { - excludedProperties := []string{"guardrails", "preview", "check", "draft", "enforce", "akas"} + excludedProperties := []string{"guardrails", "preview", "check", "draft", "enforce", "detach", "akas"} return helpers.RemoveProperties(campaignProperties, excludedProperties) } @@ -69,35 +67,35 @@ func resourceTurbotCampaign() *schema.Resource { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: phaseSchema(), + Schema: campaignPhaseSchema(), }, }, "detach": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: phaseSchema(), + Schema: campaignPhaseSchema(), }, }, "draft": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: phaseSchema(), + Schema: campaignDraftSchema(), }, }, "enforce": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: phaseSchema(), + Schema: campaignPhaseSchema(), }, }, "preview": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: phaseSchema(), + Schema: campaignPhaseSchema(), }, }, "akas": { @@ -112,7 +110,7 @@ func resourceTurbotCampaign() *schema.Resource { } } -func phaseSchema() map[string]*schema.Schema { +func campaignPhaseSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ "start_at": { Type: schema.TypeString, @@ -134,6 +132,15 @@ func phaseSchema() map[string]*schema.Schema { } } +func campaignDraftSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "start_at": { + Type: schema.TypeString, + Required: true, + }, + } +} + func resourceTurbotCampaignExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { client := meta.(*apiClient.Client) id := d.Id() @@ -156,9 +163,22 @@ func resourceTurbotCampaignCreate(d *schema.ResourceData, meta interface{}) erro phases["check"] = setPhaseAttribute(input, "check") delete(input, "check") } - input["phases"] = phases - // panic(fmt.Sprintf("HERE >>> %+v", input)) + if !isNil(input["enforce"]) { + phases["enforce"] = setPhaseAttribute(input, "enforce") + delete(input, "enforce") + } + + if !isNil(input["detach"]) { + phases["detach"] = setPhaseAttribute(input, "detach") + delete(input, "detach") + } + + if !isNil(input["draft"]) { + phases["draft"] = setDraftInputAttribute(input, "draft") + delete(input, "draft") + } + input["phases"] = phases campaign, err := client.CreateCampaign(input) if err != nil { @@ -225,8 +245,25 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error d.Set("parent", campaign.Turbot.ParentId) } - d.Set("preview", []interface{}{campaign.Phases.Preview}) - d.Set("check", []interface{}{campaign.Phases.Check}) + if campaign.Phases.Preview != nil { + d.Set("preview", []interface{}{campaign.Phases.Preview}) + } + + if campaign.Phases.Check != nil { + d.Set("check", []interface{}{campaign.Phases.Check}) + } + + if campaign.Phases.Enforce != nil { + d.Set("enforce", []interface{}{campaign.Phases.Enforce}) + } + + if campaign.Phases.Detach != nil { + d.Set("detach", []interface{}{campaign.Phases.Detach}) + } + + if campaign.Phases.Draft != nil { + d.Set("draft", []interface{}{campaign.Phases.Draft}) + } return nil } @@ -252,27 +289,28 @@ func resourceTurbotCampaignImport(d *schema.ResourceData, meta interface{}) ([]* return []*schema.ResourceData{d}, nil } -func isNil(i interface{}) bool { - if i == nil { - return true - } - switch reflect.TypeOf(i).Kind() { - case reflect.Ptr, reflect.Map, reflect.Chan, reflect.Slice: - return reflect.ValueOf(i).IsNil() - } - return false -} - func setPhaseAttribute(input map[string]interface{}, attributeName string) map[string]interface{} { phaseList := input[attributeName].([]interface{}) if len(phaseList) > 0 { phase := phaseList[0].(map[string]interface{}) return map[string]interface{}{ - "transitionAt": phase["start_at"].(string), - "transitionNotice": phase["start_notice"].(string), - "transitionWhen": phase["start_early_if"].(string), - "warnAt": phase["warn_at"].([]interface{}), + "startAt": phase["start_at"].(string), + "startNotice": phase["start_notice"].(string), + "startEarlyIf": phase["start_early_if"].(string), + "warnAt": phase["warn_at"].([]interface{}), + } + } + return nil +} + +func setDraftInputAttribute(input map[string]interface{}, attributeName string) map[string]interface{} { + draftInputs := input[attributeName].([]interface{}) + if len(draftInputs) > 0 { + draft := draftInputs[0].(map[string]interface{}) + + return map[string]interface{}{ + "startAt": draft["start_at"].(string), } } return nil From 1a22b73dbb6bff5c1a1470c635c1d4c7796b831e Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Thu, 3 Apr 2025 18:20:44 +0530 Subject: [PATCH 05/11] Refactor read function --- turbot/resource_turbot_campaign.go | 52 ++++++++++++++---------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index dfc5fdd6..e6f2dea3 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -221,49 +221,45 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error return err } + // Set basic attributes d.Set("description", campaign.Description) d.Set("status", campaign.Status) d.Set("title", campaign.Turbot.Title) d.Set("recipients", campaign.Recipients) d.Set("akas", campaign.Turbot.Akas) - if len(campaign.Accounts.Items) > 0 { - accounts := []string{} - for _, item := range campaign.Accounts.Items { - accounts = append(accounts, item.Turbot.Id) + // Helper to extract Turbot.Id from a slice of items + extractIds := func(items []struct { + Turbot apiClient.TurbotResourceMetadata + }) []string { + ids := make([]string, len(items)) + for i, item := range items { + ids[i] = item.Turbot.Id } - d.Set("accounts", accounts) + return ids + } + + if len(campaign.Accounts.Items) > 0 { + d.Set("accounts", extractIds(campaign.Accounts.Items)) } if len(campaign.Guardrails.Items) > 0 { - guardrails := []string{} - for _, item := range campaign.Guardrails.Items { - guardrails = append(guardrails, item.Turbot.Id) - } - d.Set("guardrails", guardrails) + d.Set("guardrails", extractIds(campaign.Guardrails.Items)) } if !isNil(campaign.Turbot.ParentId) { d.Set("parent", campaign.Turbot.ParentId) } - if campaign.Phases.Preview != nil { - d.Set("preview", []interface{}{campaign.Phases.Preview}) - } - - if campaign.Phases.Check != nil { - d.Set("check", []interface{}{campaign.Phases.Check}) - } - - if campaign.Phases.Enforce != nil { - d.Set("enforce", []interface{}{campaign.Phases.Enforce}) - } - - if campaign.Phases.Detach != nil { - d.Set("detach", []interface{}{campaign.Phases.Detach}) - } - - if campaign.Phases.Draft != nil { - d.Set("draft", []interface{}{campaign.Phases.Draft}) + // Helper to set phase if it's not nil + setPhase := func(key string, phase interface{}) { + if phase != nil { + _ = d.Set(key, []interface{}{phase}) + } } + setPhase("preview", campaign.Phases.Preview) + setPhase("check", campaign.Phases.Check) + setPhase("enforce", campaign.Phases.Enforce) + setPhase("detach", campaign.Phases.Detach) + setPhase("draft", campaign.Phases.Draft) return nil } From e4785fc5278f6d10cb2fc0b1e09601fdf3443c23 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Thu, 3 Apr 2025 19:23:38 +0530 Subject: [PATCH 06/11] Add recipients to phase schema --- turbot/resource_turbot_campaign.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index e6f2dea3..b6c7ec63 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -124,6 +124,11 @@ func campaignPhaseSchema() map[string]*schema.Schema { Type: schema.TypeString, Optional: true, }, + "recipients": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, "warn_at": { Type: schema.TypeList, Optional: true, @@ -295,6 +300,7 @@ func setPhaseAttribute(input map[string]interface{}, attributeName string) map[s "startNotice": phase["start_notice"].(string), "startEarlyIf": phase["start_early_if"].(string), "warnAt": phase["warn_at"].([]interface{}), + "recipients": phase["recipients"].([]interface{}), } } return nil From c8231e8a89cd8eab786cb61481ff03cdae068cfb Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Tue, 6 May 2025 17:20:33 +0530 Subject: [PATCH 07/11] update --- turbot/resource_turbot_campaign.go | 40 +++++++++++++----------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index b6c7ec63..35c915d3 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -155,36 +155,29 @@ func resourceTurbotCampaignExists(d *schema.ResourceData, meta interface{}) (b b func resourceTurbotCampaignCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*apiClient.Client) - // build map of folder properties + // build map of campaign properties input := mapFromResourceData(d, campaignProperties) - phases := map[string]interface{}{} - if !isNil(input["preview"]) { - phases["preview"] = setPhaseAttribute(input, "preview") - delete(input, "preview") - } + // extract and set phase-related inputs + phases := make(map[string]interface{}) - if !isNil(input["check"]) { - phases["check"] = setPhaseAttribute(input, "check") - delete(input, "check") - } - - if !isNil(input["enforce"]) { - phases["enforce"] = setPhaseAttribute(input, "enforce") - delete(input, "enforce") - } - - if !isNil(input["detach"]) { - phases["detach"] = setPhaseAttribute(input, "detach") - delete(input, "detach") + phaseKeys := []string{"preview", "check", "enforce", "detach"} + for _, key := range phaseKeys { + if !isNil(input[key]) { + phases[key] = setPhaseAttribute(input, key) + delete(input, key) + } } + // special handling for "draft" if !isNil(input["draft"]) { phases["draft"] = setDraftInputAttribute(input, "draft") delete(input, "draft") } + input["phases"] = phases + // create the campaign campaign, err := client.CreateCampaign(input) if err != nil { return err @@ -233,7 +226,7 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error d.Set("recipients", campaign.Recipients) d.Set("akas", campaign.Turbot.Akas) - // Helper to extract Turbot.Id from a slice of items + // helper to extract Turbot.Id from a slice of items extractIds := func(items []struct { Turbot apiClient.TurbotResourceMetadata }) []string { @@ -254,12 +247,13 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error d.Set("parent", campaign.Turbot.ParentId) } - // Helper to set phase if it's not nil + // helper to set phase if it's not nil setPhase := func(key string, phase interface{}) { - if phase != nil { - _ = d.Set(key, []interface{}{phase}) + if !isNil(phase) { + d.Set(key, []interface{}{phase}) } } + setPhase("preview", campaign.Phases.Preview) setPhase("check", campaign.Phases.Check) setPhase("enforce", campaign.Phases.Enforce) From dd0376dfc1a1cf7ad73578f813de6b53b942f92d Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Fri, 13 Jun 2025 17:23:34 +0530 Subject: [PATCH 08/11] Renamed campaign to rollout --- CHANGELOG.md | 2 +- apiClient/campaign.go | 30 ++--- apiClient/queries.go | 18 +-- apiClient/types.go | 33 +++--- turbot/provider.go | 2 +- turbot/resource_turbot_campaign.go | 144 +++++++++++++++--------- turbot/resource_turbot_campaign_test.go | 48 ++++---- 7 files changed, 163 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0dda5f2d..f77ec8c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ FEATURES: -* **New Resource:** `turbot_campaign` ([#195](https://github.com/turbot/terraform-provider-turbot/issues/195)) +* **New Resource:** `turbot_rollout` ([#195](https://github.com/turbot/terraform-provider-turbot/issues/195)) ## 1.12.3 (TBD) diff --git a/apiClient/campaign.go b/apiClient/campaign.go index 88ed687e..34a5ed7f 100644 --- a/apiClient/campaign.go +++ b/apiClient/campaign.go @@ -1,39 +1,39 @@ package apiClient -func (client *Client) CreateCampaign(input map[string]interface{}) (*Campaign, error) { - query := createCampaignMutation() - responseData := &CampaignResponse{} +func (client *Client) CreateRollout(input map[string]interface{}) (*Rollout, error) { + query := createRolloutMutation() + responseData := &RolloutResponse{} variables := map[string]interface{}{ "input": input, } // execute api call if err := client.doRequest(query, variables, responseData); err != nil { - return nil, client.handleCreateError(err, input, "campaign") + return nil, client.handleCreateError(err, input, "rollout") } - return &responseData.Campaign, nil + return &responseData.Rollout, nil } -func (client *Client) ReadCampaign(id string) (*Campaign, error) { - query := readCampaignQuery(id) - responseData := &CampaignResponse{} +func (client *Client) ReadRollout(id string) (*Rollout, error) { + query := readRolloutQuery(id) + responseData := &RolloutResponse{} // execute api call if err := client.doRequest(query, nil, responseData); err != nil { - return nil, client.handleReadError(err, id, "campaign") + return nil, client.handleReadError(err, id, "rollout") } - return &responseData.Campaign, nil + return &responseData.Rollout, nil } -func (client *Client) UpdateCampaign(input map[string]interface{}) (*Campaign, error) { - query := updateCampaignMutation() - responseData := &CampaignResponse{} +func (client *Client) UpdateRollout(input map[string]interface{}) (*Rollout, error) { + query := updateRolloutMutation() + responseData := &RolloutResponse{} variables := map[string]interface{}{ "input": input, } // execute api call if err := client.doRequest(query, variables, responseData); err != nil { - return nil, client.handleUpdateError(err, input, "campaign") + return nil, client.handleUpdateError(err, input, "rollout") } - return &responseData.Campaign, nil + return &responseData.Rollout, nil } diff --git a/apiClient/queries.go b/apiClient/queries.go index b425fd5d..97c312ff 100644 --- a/apiClient/queries.go +++ b/apiClient/queries.go @@ -747,11 +747,11 @@ func (client *Client) GetTurbotWorkspaceVersion() (*semver.Version, error) { return version, nil } -// campaign +// rollout // filter and description are removed for a workaround, will be removed after a Core change. -func createCampaignMutation() string { - return `mutation CreateCampaign($input: CreateCampaignInput!) { - campaign: createCampaign(input: $input) { +func createRolloutMutation() string { + return `mutation CreateRollout($input: CreateRolloutInput!) { + rollout: createRollout(input: $input) { title status description @@ -781,9 +781,9 @@ func createCampaignMutation() string { }` } -func readCampaignQuery(id string) string { +func readRolloutQuery(id string) string { return fmt.Sprintf(`{ - campaign(id: "%s") { + rollout(id: "%s") { description status title @@ -813,9 +813,9 @@ func readCampaignQuery(id string) string { }`, id) } -func updateCampaignMutation() string { - return `mutation UpdateCampaign($input: UpdateCampaignInput!) { - campaign: updateCampaign(input: $input) { +func updateRolloutMutation() string { + return `mutation UpdateRollout($input: UpdateRolloutInput!) { + rollout: updateRollout(input: $input) { title status description diff --git a/apiClient/types.go b/apiClient/types.go index 6ad8629a..242a14c5 100644 --- a/apiClient/types.go +++ b/apiClient/types.go @@ -536,12 +536,12 @@ type TurbotWatchMetadata struct { FavoriteId string } -// Campaign -type CampaignResponse struct { - Campaign Campaign +// Rollout +type RolloutResponse struct { + Rollout Rollout } -type Campaign struct { +type Rollout struct { Turbot TurbotResourceMetadata Description string Status string @@ -557,18 +557,18 @@ type Campaign struct { Turbot TurbotResourceMetadata } } - Phases CampaignPhases `json:"phases"` + Phases RolloutPhases `json:"phases"` } -type CampaignPhases struct { - Draft *TurbotCampaignDraftPhaseMetadata `json:"draft,omitempty"` - Preview *TurbotCampaignPhaseMetadata `json:"preview,omitempty"` - Check *TurbotCampaignPhaseMetadata `json:"check,omitempty"` - Enforce *TurbotCampaignPhaseMetadata `json:"enforce,omitempty"` - Detach *TurbotCampaignPhaseMetadata `json:"detach,omitempty"` +type RolloutPhases struct { + Draft *TurbotRolloutDraftPhaseMetadata `json:"draft,omitempty"` + Preview *TurbotRolloutPreviewPhaseMetadata `json:"preview,omitempty"` + Check *TurbotRolloutPhaseMetadata `json:"check,omitempty"` + Enforce *TurbotRolloutPhaseMetadata `json:"enforce,omitempty"` + Detach *TurbotRolloutPhaseMetadata `json:"detach,omitempty"` } -type TurbotCampaignPhaseMetadata struct { +type TurbotRolloutPhaseMetadata struct { StartAt string `json:"startAt,omitempty"` StartNotice string `json:"startNotice,omitempty"` StartEarlyIf string `json:"startEarlyIf,omitempty"` @@ -576,6 +576,13 @@ type TurbotCampaignPhaseMetadata struct { Recipients []string `json:"recipients,omitempty"` } -type TurbotCampaignDraftPhaseMetadata struct { +type TurbotRolloutDraftPhaseMetadata struct { StartAt string `json:"startAt,omitempty"` } + +type TurbotRolloutPreviewPhaseMetadata struct { + StartAt string `json:"startAt,omitempty"` + StartNotice string `json:"startNotice,omitempty"` + StartEarlyIf string `json:"startEarlyIf,omitempty"` + Recipients []string `json:"recipients,omitempty"` +} diff --git a/turbot/provider.go b/turbot/provider.go index 364ca44d..b66cd0f9 100755 --- a/turbot/provider.go +++ b/turbot/provider.go @@ -35,7 +35,7 @@ func Provider() terraform.ResourceProvider { }, ResourcesMap: map[string]*schema.Resource{ - "turbot_campaign": resourceTurbotCampaign(), + "turbot_rollout": resourceTurbotRollout(), "turbot_control_mute": resourceTurbotControlMute(), "turbot_file": resourceTurbotFile(), "turbot_folder": resourceTurbotFolder(), diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_campaign.go index 35c915d3..cdb168f3 100644 --- a/turbot/resource_turbot_campaign.go +++ b/turbot/resource_turbot_campaign.go @@ -8,22 +8,22 @@ import ( ) // properties which must be passed to a create/update call -var campaignProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "detach", "guardrails", "accounts", "akas"} +var rolloutProperties = []interface{}{"title", "description", "status", "recipients", "preview", "check", "draft", "enforce", "detach", "guardrails", "accounts", "akas"} -func getCampaignUpdateProperties() []interface{} { +func getRolloutUpdateProperties() []interface{} { excludedProperties := []string{"guardrails", "preview", "check", "draft", "enforce", "detach", "akas"} - return helpers.RemoveProperties(campaignProperties, excludedProperties) + return helpers.RemoveProperties(rolloutProperties, excludedProperties) } -func resourceTurbotCampaign() *schema.Resource { +func resourceTurbotRollout() *schema.Resource { return &schema.Resource{ - Create: resourceTurbotCampaignCreate, - Read: resourceTurbotCampaignRead, - Update: resourceTurbotCampaignUpdate, - Delete: resourceTurbotCampaignDelete, - Exists: resourceTurbotCampaignExists, + Create: resourceTurbotRolloutCreate, + Read: resourceTurbotRolloutRead, + Update: resourceTurbotRolloutUpdate, + Delete: resourceTurbotRolloutDelete, + Exists: resourceTurbotRolloutExists, Importer: &schema.ResourceImporter{ - State: resourceTurbotCampaignImport, + State: resourceTurbotRolloutImport, }, Schema: map[string]*schema.Schema{ "guardrails": { @@ -67,35 +67,35 @@ func resourceTurbotCampaign() *schema.Resource { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: campaignPhaseSchema(), + Schema: rolloutPhaseSchema(), }, }, "detach": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: campaignPhaseSchema(), + Schema: rolloutPhaseSchema(), }, }, "draft": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: campaignDraftSchema(), + Schema: rolloutDraftSchema(), }, }, "enforce": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: campaignPhaseSchema(), + Schema: rolloutPhaseSchema(), }, }, "preview": { Type: schema.TypeList, Optional: true, Elem: &schema.Resource{ - Schema: campaignPhaseSchema(), + Schema: rolloutPreviewSchema(), }, }, "akas": { @@ -110,7 +110,7 @@ func resourceTurbotCampaign() *schema.Resource { } } -func campaignPhaseSchema() map[string]*schema.Schema { +func rolloutPhaseSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ "start_at": { Type: schema.TypeString, @@ -137,7 +137,7 @@ func campaignPhaseSchema() map[string]*schema.Schema { } } -func campaignDraftSchema() map[string]*schema.Schema { +func rolloutDraftSchema() map[string]*schema.Schema { return map[string]*schema.Schema{ "start_at": { Type: schema.TypeString, @@ -146,22 +146,44 @@ func campaignDraftSchema() map[string]*schema.Schema { } } -func resourceTurbotCampaignExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { +func rolloutPreviewSchema() map[string]*schema.Schema { + return map[string]*schema.Schema{ + "start_at": { + Type: schema.TypeString, + Required: true, + }, + "start_notice": { + Type: schema.TypeString, + Optional: true, + }, + "start_early_if": { + Type: schema.TypeString, + Optional: true, + }, + "recipients": { + Type: schema.TypeList, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + } +} + +func resourceTurbotRolloutExists(d *schema.ResourceData, meta interface{}) (b bool, e error) { client := meta.(*apiClient.Client) id := d.Id() return client.ResourceExists(id) } -func resourceTurbotCampaignCreate(d *schema.ResourceData, meta interface{}) error { +func resourceTurbotRolloutCreate(d *schema.ResourceData, meta interface{}) error { client := meta.(*apiClient.Client) - // build map of campaign properties - input := mapFromResourceData(d, campaignProperties) + // build map of rollout properties + input := mapFromResourceData(d, rolloutProperties) // extract and set phase-related inputs phases := make(map[string]interface{}) - phaseKeys := []string{"preview", "check", "enforce", "detach"} + phaseKeys := []string{"check", "enforce", "detach"} for _, key := range phaseKeys { if !isNil(input[key]) { phases[key] = setPhaseAttribute(input, key) @@ -175,42 +197,47 @@ func resourceTurbotCampaignCreate(d *schema.ResourceData, meta interface{}) erro delete(input, "draft") } + if !isNil(input["preview"]) { + phases["preview"] = setPreviewInputAttribute(input, "preview") + delete(input, "preview") + } + input["phases"] = phases - // create the campaign - campaign, err := client.CreateCampaign(input) + // create the rollout + rollout, err := client.CreateRollout(input) if err != nil { return err } // assign the id - d.SetId(campaign.Turbot.Id) + d.SetId(rollout.Turbot.Id) // TODO Remove Read call once schema changes are In. - return resourceTurbotCampaignRead(d, meta) + return resourceTurbotRolloutRead(d, meta) } -func resourceTurbotCampaignUpdate(d *schema.ResourceData, meta interface{}) error { +func resourceTurbotRolloutUpdate(d *schema.ResourceData, meta interface{}) error { client := meta.(*apiClient.Client) id := d.Id() // build map of folder properties - input := mapFromResourceData(d, getCampaignUpdateProperties()) + input := mapFromResourceData(d, getRolloutUpdateProperties()) input["id"] = id - _, err := client.UpdateCampaign(input) + _, err := client.UpdateRollout(input) if err != nil { return err } // set 'Read' Properties - return resourceTurbotCampaignRead(d, meta) + return resourceTurbotRolloutRead(d, meta) } -func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error { +func resourceTurbotRolloutRead(d *schema.ResourceData, meta interface{}) error { client := meta.(*apiClient.Client) id := d.Id() - campaign, err := client.ReadCampaign(id) + rollout, err := client.ReadRollout(id) if err != nil { if errors.NotFoundError(err) { // folder was not found - clear id @@ -220,11 +247,11 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error } // Set basic attributes - d.Set("description", campaign.Description) - d.Set("status", campaign.Status) - d.Set("title", campaign.Turbot.Title) - d.Set("recipients", campaign.Recipients) - d.Set("akas", campaign.Turbot.Akas) + d.Set("description", rollout.Description) + d.Set("status", rollout.Status) + d.Set("title", rollout.Turbot.Title) + d.Set("recipients", rollout.Recipients) + d.Set("akas", rollout.Turbot.Akas) // helper to extract Turbot.Id from a slice of items extractIds := func(items []struct { @@ -237,14 +264,14 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error return ids } - if len(campaign.Accounts.Items) > 0 { - d.Set("accounts", extractIds(campaign.Accounts.Items)) + if len(rollout.Accounts.Items) > 0 { + d.Set("accounts", extractIds(rollout.Accounts.Items)) } - if len(campaign.Guardrails.Items) > 0 { - d.Set("guardrails", extractIds(campaign.Guardrails.Items)) + if len(rollout.Guardrails.Items) > 0 { + d.Set("guardrails", extractIds(rollout.Guardrails.Items)) } - if !isNil(campaign.Turbot.ParentId) { - d.Set("parent", campaign.Turbot.ParentId) + if !isNil(rollout.Turbot.ParentId) { + d.Set("parent", rollout.Turbot.ParentId) } // helper to set phase if it's not nil @@ -254,16 +281,16 @@ func resourceTurbotCampaignRead(d *schema.ResourceData, meta interface{}) error } } - setPhase("preview", campaign.Phases.Preview) - setPhase("check", campaign.Phases.Check) - setPhase("enforce", campaign.Phases.Enforce) - setPhase("detach", campaign.Phases.Detach) - setPhase("draft", campaign.Phases.Draft) + setPhase("preview", rollout.Phases.Preview) + setPhase("check", rollout.Phases.Check) + setPhase("enforce", rollout.Phases.Enforce) + setPhase("detach", rollout.Phases.Detach) + setPhase("draft", rollout.Phases.Draft) return nil } -func resourceTurbotCampaignDelete(d *schema.ResourceData, meta interface{}) error { +func resourceTurbotRolloutDelete(d *schema.ResourceData, meta interface{}) error { client := meta.(*apiClient.Client) id := d.Id() err := client.DeleteResource(id) @@ -277,8 +304,8 @@ func resourceTurbotCampaignDelete(d *schema.ResourceData, meta interface{}) erro return nil } -func resourceTurbotCampaignImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { - if err := resourceTurbotCampaignRead(d, meta); err != nil { +func resourceTurbotRolloutImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + if err := resourceTurbotRolloutRead(d, meta); err != nil { return nil, err } return []*schema.ResourceData{d}, nil @@ -311,3 +338,18 @@ func setDraftInputAttribute(input map[string]interface{}, attributeName string) } return nil } + +func setPreviewInputAttribute(input map[string]interface{}, attributeName string) map[string]interface{} { + previewInputs := input[attributeName].([]interface{}) + if len(previewInputs) > 0 { + preview := previewInputs[0].(map[string]interface{}) + + return map[string]interface{}{ + "startAt": preview["start_at"].(string), + "startNotice": preview["start_notice"].(string), + "startEarlyIf": preview["start_early_if"].(string), + "recipients": preview["recipients"].([]interface{}), + } + } + return nil +} diff --git a/turbot/resource_turbot_campaign_test.go b/turbot/resource_turbot_campaign_test.go index 170eab5a..7e7feb84 100644 --- a/turbot/resource_turbot_campaign_test.go +++ b/turbot/resource_turbot_campaign_test.go @@ -11,27 +11,27 @@ import ( ) // test suites -func TestAccCampaign_Basic(t *testing.T) { - resourceName := "turbot_campaign.test" +func TestAccRollout_Basic(t *testing.T) { + resourceName := "turbot_rollout.test" resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, Providers: testAccProviders, - CheckDestroy: testAccCheckCampaignDestroy, + CheckDestroy: testAccCheckRolloutDestroy, Steps: []resource.TestStep{ { - Config: testAccCampaignConfig(), + Config: testAccRolloutConfig(), Check: resource.ComposeTestCheckFunc( - testAccCheckCampaignExists("turbot_campaign.test"), - resource.TestCheckResourceAttr("turbot_campaign.test", "title", "Test Campaign Resource Created Through Terraform"), - resource.TestCheckResourceAttr("turbot_campaign.test", "description", "Campaign For Testing"), + testAccCheckRolloutExists("turbot_rollout.test"), + resource.TestCheckResourceAttr("turbot_rollout.test", "title", "Test Rollout Resource Created Through Terraform"), + resource.TestCheckResourceAttr("turbot_rollout.test", "description", "Rollout For Testing"), ), }, { - Config: testAccCampaignUpdateDescConfig(), + Config: testAccRolloutUpdateDescConfig(), Check: resource.ComposeTestCheckFunc( - testAccCheckCampaignExists("turbot_campaign.test"), - resource.TestCheckResourceAttr("turbot_campaign.test", "title", "Test Campaign Resource Created Through Terraform"), - resource.TestCheckResourceAttr("turbot_campaign.test", "description", "Campaign For Testing Updated"), + testAccCheckRolloutExists("turbot_rollout.test"), + resource.TestCheckResourceAttr("turbot_rollout.test", "title", "Test Rollout Resource Created Through Terraform"), + resource.TestCheckResourceAttr("turbot_rollout.test", "description", "Rollout For Testing Updated"), ), }, { @@ -45,12 +45,12 @@ func TestAccCampaign_Basic(t *testing.T) { } // configs -func testAccCampaignConfig() string { +func testAccRolloutConfig() string { return ` -resource "turbot_campaign" "test" { +resource "turbot_rollout" "test" { - title = "Test Campaign Resource Created Through Terraform" - description = "Campaign For Testing" + title = "Test Rollout Resource Created Through Terraform" + description = "Rollout For Testing" guardrails = ["348351678444334"] accounts = ["330101957430965"] @@ -64,12 +64,12 @@ resource "turbot_campaign" "test" { ` } -func testAccCampaignUpdateDescConfig() string { +func testAccRolloutUpdateDescConfig() string { return ` -resource "turbot_campaign" "test" { +resource "turbot_rollout" "test" { - title = "Test Campaign Resource Created Through Terraform" - description = "Campaign For Testing Updated" + title = "Test Rollout Resource Created Through Terraform" + description = "Rollout For Testing Updated" guardrails = ["348351678444334"] accounts = ["330101957430965"] @@ -84,7 +84,7 @@ resource "turbot_campaign" "test" { } // helper functions -func testAccCheckCampaignExists(resource string) resource.TestCheckFunc { +func testAccCheckRolloutExists(resource string) resource.TestCheckFunc { return func(state *terraform.State) error { rs, ok := state.RootModule().Resources[resource] if !ok { @@ -94,7 +94,7 @@ func testAccCheckCampaignExists(resource string) resource.TestCheckFunc { return fmt.Errorf("no Record ID is set") } client := testAccProvider.Meta().(*apiClient.Client) - _, err := client.ReadCampaign(rs.Primary.ID) + _, err := client.ReadRollout(rs.Primary.ID) if err != nil { return fmt.Errorf("error fetching item with resource %s. %s", resource, err) } @@ -102,11 +102,11 @@ func testAccCheckCampaignExists(resource string) resource.TestCheckFunc { } } -func testAccCheckCampaignDestroy(s *terraform.State) error { +func testAccCheckRolloutDestroy(s *terraform.State) error { client := testAccProvider.Meta().(*apiClient.Client) for _, rs := range s.RootModule().Resources { - if rs.Type == "turbot_campaign" { - _, err := client.ReadCampaign(rs.Primary.ID) + if rs.Type == "turbot_rollout" { + _, err := client.ReadRollout(rs.Primary.ID) if err == nil { return fmt.Errorf("alert still exists") } From acd1cfb470305e42efbdc4a6532f6dad768d645f Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Fri, 13 Jun 2025 18:40:48 +0530 Subject: [PATCH 09/11] Renamed file --- .../{resource_turbot_campaign.go => resource_turbot_rollout.go} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename turbot/{resource_turbot_campaign.go => resource_turbot_rollout.go} (100%) diff --git a/turbot/resource_turbot_campaign.go b/turbot/resource_turbot_rollout.go similarity index 100% rename from turbot/resource_turbot_campaign.go rename to turbot/resource_turbot_rollout.go From 8db35407c9374d9e649317ecb38f81dc5f905225 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Fri, 13 Jun 2025 18:57:53 +0530 Subject: [PATCH 10/11] Add docs --- website/docs/r/rollout.html.markdown | 99 ++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 website/docs/r/rollout.html.markdown diff --git a/website/docs/r/rollout.html.markdown b/website/docs/r/rollout.html.markdown new file mode 100644 index 00000000..be2c84a9 --- /dev/null +++ b/website/docs/r/rollout.html.markdown @@ -0,0 +1,99 @@ +--- +layout: "turbot" +title: "turbot" +template: Documentation +page_title: "Turbot: turbot_rollout" +nav: + title: turbot_rollout +--- + +# turbot_rollout + +`Turbot Rollout` allows you to define and automate the rollout of one or more guardrails across one or more accounts through a series of controlled phases (e.g., preview, check, enforce, detach), with scheduled transitions and optional communications. + +## Example Usage + +**Automated Rollout with Scheduled Transitions** + +```hcl +resource "turbot_rollout" "rollout" { + + title = "Test Rollout Created Through Terraform" + description = "This is a test rollout created through terraform." + + guardrails = ["356127854694770"] + accounts = ["350804102494366"] + recipients = ["Account/*", "Turbot/Owner", "Turbot/Admin"] + akas = ["terraform_test_rollout"] + status = "ACTIVE" + + preview { + start_at = "2025-12-29T00:00:00.000Z" + start_early_if = "no_alerts" + start_notice = "enabled" + } + + check { + start_at = "2025-11-30T00:00:00.000Z" + warn_at = ["2025-10-23T00:00:00.000Z", "2025-10-29T00:00:00.000Z"] + start_early_if = "no_alerts" + start_notice = "enabled" + } + + draft { + start_at = "2025-08-30T00:00:00.000Z" + } +} +``` + +## Argument Reference + +The following arguments are supported: + +- `title` - (Required) A short display name for the rollout. +- `akas` - (Optional) Unique identifier of the resource. +- `description` - (Optional) Description of the rollout’s purpose. +- `guardrails` - (Optional) List of guardrail IDs or AKAs to include in the rollout. +- `accounts` - (Optional) List of account IDs or AKAs the rollout will target. +- `recipients` - (Optional) List of recipients to notify. Can include notification profiles. +- `status` - (Optional) Current status of the rollout. Must be ACTIVE to initiate phase transitions. + +## Phase Blocks +At least one phase block is required. Each phase corresponds to a step in the rollout lifecycle. The valid phases are: `draft`, `preview`, `check`, `enforce`, and `detach`. + +### `draft` Block + +This is the initial phase and typically used for staging purposes. It supports: +- start_at – (Optional) Absolute timestamp when the rollout should transition to the draft phase. + +### `preview` Block + +This phase is used to introduce changes to stakeholders before enforcing them. It supports: +- start_at – (Optional) Timestamp when accounts should enter the preview phase. +- start_notice – (Optional) Whether to send welcome notices on entry. One of enabled, disabled (default). +- start_early_if – (Optional) Set to "no_alerts" to allow accounts to enter the phase early if no alerts are present. +- recipients – (Optional) Overrides the default recipients for this phase. + +### `check`, `enforce`, `detach` Blocks + +These phases support the full set of scheduling and communication options: +In addition to all the arguments above, the following attributes are exported: + +- start_at – (Optional) Timestamp when accounts should enter the preview phase. +- warn_at – (Optional) List of timestamps to send warning notices before the phase starts. +- start_notice – (Optional) Whether to send welcome notices on entry. One of enabled, disabled (default). +- start_early_if – (Optional) Set to "no_alerts" to allow accounts to enter the phase early if no alerts are present. +- recipients – (Optional) Overrides the default recipients for this phase. + +## Attributes Reference + +- `parent` - The id of the rollout’s parent resource. +- `id` - Unique identifier of the resource. + +## Import + +Rollouts can be imported using the id. For example: + +``` +terraform import turbot_rollout.rollout 123456789012 +``` From c25b89b87041e1a773157de0a3c589ca18648aa2 Mon Sep 17 00:00:00 2001 From: Subhajit Mondal Date: Fri, 13 Jun 2025 19:03:16 +0530 Subject: [PATCH 11/11] Renamed test files --- apiClient/{campaign.go => rollout.go} | 0 ...ce_turbot_campaign_test.go => resource_turbot_rollout_test.go} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename apiClient/{campaign.go => rollout.go} (100%) rename turbot/{resource_turbot_campaign_test.go => resource_turbot_rollout_test.go} (100%) diff --git a/apiClient/campaign.go b/apiClient/rollout.go similarity index 100% rename from apiClient/campaign.go rename to apiClient/rollout.go diff --git a/turbot/resource_turbot_campaign_test.go b/turbot/resource_turbot_rollout_test.go similarity index 100% rename from turbot/resource_turbot_campaign_test.go rename to turbot/resource_turbot_rollout_test.go