Skip to content

Commit 46d74b1

Browse files
authored
Merge pull request #818 from SumoLogic/CSOAR-3990
CSOAR-3990: Add Terraform support for CSOAR Playbooks
2 parents 8142a6d + df1d148 commit 46d74b1

File tree

6 files changed

+651
-1
lines changed

6 files changed

+651
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ ENHANCEMENTS:
99

1010
DOCS:
1111
* Updated monitor documentation with a few trigger condition fields that were missing.
12+
* Added support to manage CSOAR playbooks.
1213
FEATURES:
13-
* **New Resource:** sumologic_azure_metrics_source (GH-710), macros (SUMO-260843)
14+
* **New Resource:** sumologic_azure_metrics_source (GH-710), macros (SUMO-260843), sumologic_csoar_playbook (CSOAR-3990)
1415

1516
## 3.1.5 (September 9, 2025)
1617

sumologic/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func Provider() *schema.Provider {
6666
"sumologic_cse_threshold_rule": resourceSumologicCSEThresholdRule(),
6767
"sumologic_cse_first_seen_rule": resourceSumologicCSEFirstSeenRule(),
6868
"sumologic_cse_outlier_rule": resourceSumologicCSEOutlierRule(),
69+
"sumologic_csoar_playbook": resourceSumologicCsoarPlaybook(),
6970
"sumologic_collector": resourceSumologicCollector(),
7071
"sumologic_installed_collector": resourceSumologicInstalledCollector(),
7172
"sumologic_http_source": resourceSumologicHTTPSource(),
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
package sumologic
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
7+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
8+
)
9+
10+
func resourceSumologicCsoarPlaybook() *schema.Resource {
11+
return &schema.Resource{
12+
Create: resourceSumologicCsoarPlaybookCreate,
13+
Read: resourceSumologicCsoarPlaybookRead,
14+
Update: resourceSumologicCsoarPlaybookUpdate,
15+
Delete: resourceSumologicCsoarPlaybookDelete,
16+
Importer: &schema.ResourceImporter{
17+
State: resourceSumologicCsoarPlaybookImport,
18+
},
19+
20+
Schema: map[string]*schema.Schema{
21+
"name": {
22+
Type: schema.TypeString,
23+
Required: true,
24+
},
25+
"description": {
26+
Type: schema.TypeString,
27+
Optional: true,
28+
},
29+
"updated_name": {
30+
Type: schema.TypeString,
31+
Optional: true,
32+
},
33+
"tags": {
34+
Type: schema.TypeString,
35+
Optional: true,
36+
},
37+
"is_deleted": {
38+
Type: schema.TypeBool,
39+
Optional: true,
40+
Default: false,
41+
},
42+
"draft": {
43+
Type: schema.TypeBool,
44+
Optional: true,
45+
Default: false,
46+
},
47+
"is_published": {
48+
Type: schema.TypeBool,
49+
Optional: true,
50+
Default: false,
51+
},
52+
"last_updated": {
53+
Type: schema.TypeInt,
54+
Computed: true, // This should be set by the API, not by user
55+
},
56+
"created_by": {
57+
Type: schema.TypeInt,
58+
Computed: true, // This should be set by the API, not by user
59+
},
60+
"updated_by": {
61+
Type: schema.TypeInt,
62+
Computed: true, // This should be set by the API, not by user
63+
},
64+
"nested": {
65+
Type: schema.TypeBool,
66+
Optional: true,
67+
Default: false,
68+
},
69+
"type": {
70+
Type: schema.TypeString,
71+
Optional: true,
72+
},
73+
"is_enabled": {
74+
Type: schema.TypeBool,
75+
Optional: true,
76+
Default: true,
77+
},
78+
"links": {
79+
Type: schema.TypeString,
80+
Optional: true,
81+
Description: "JSON string representation of playbook links",
82+
},
83+
"nodes": {
84+
Type: schema.TypeString,
85+
Optional: true,
86+
Description: "JSON string representation of playbook nodes",
87+
},
88+
},
89+
}
90+
}
91+
92+
func resourceSumologicCsoarPlaybookDelete(d *schema.ResourceData, meta interface{}) error {
93+
c := meta.(*Client)
94+
name := d.Get("name").(string)
95+
return c.DeletePlaybook(name)
96+
}
97+
98+
func resourceSumologicCsoarPlaybookUpdate(d *schema.ResourceData, meta interface{}) error {
99+
c := meta.(*Client)
100+
101+
playbook := Playbook{
102+
Name: d.Get("name").(string),
103+
}
104+
105+
playbook.Description = d.Get("description").(string)
106+
playbook.UpdatedName = d.Get("updated_name").(string)
107+
playbook.Tags = d.Get("tags").(string)
108+
playbook.Type = d.Get("type").(string)
109+
playbook.IsDeleted = d.Get("is_deleted").(bool)
110+
playbook.Draft = d.Get("draft").(bool)
111+
playbook.IsPublished = d.Get("is_published").(bool)
112+
playbook.Nested = d.Get("nested").(bool)
113+
playbook.IsEnabled = d.Get("is_enabled").(bool)
114+
115+
// Handle links as JSON string
116+
if linksJSON, ok := d.Get("links").(string); ok && linksJSON != "" {
117+
var links []map[string]interface{}
118+
if err := json.Unmarshal([]byte(linksJSON), &links); err != nil {
119+
return fmt.Errorf("error parsing links JSON: %v", err)
120+
}
121+
playbook.Links = links
122+
}
123+
124+
// Handle nodes as JSON string
125+
if nodesJSON, ok := d.Get("nodes").(string); ok && nodesJSON != "" {
126+
var nodes []map[string]interface{}
127+
if err := json.Unmarshal([]byte(nodesJSON), &nodes); err != nil {
128+
return fmt.Errorf("error parsing nodes JSON: %v", err)
129+
}
130+
playbook.Nodes = nodes
131+
}
132+
133+
err := c.UpdatePlaybook(playbook)
134+
if err != nil {
135+
return err
136+
}
137+
138+
// Always set state after successful update
139+
d.Set("description", playbook.Description)
140+
d.Set("tags", playbook.Tags)
141+
d.Set("updated_name", playbook.UpdatedName)
142+
d.Set("type", playbook.Type)
143+
d.Set("is_deleted", playbook.IsDeleted)
144+
d.Set("draft", playbook.Draft)
145+
d.Set("is_published", playbook.IsPublished)
146+
d.Set("nested", playbook.Nested)
147+
d.Set("is_enabled", playbook.IsEnabled)
148+
149+
linksJSON := d.Get("links").(string)
150+
d.Set("links", linksJSON)
151+
152+
nodesJSON := d.Get("nodes").(string)
153+
d.Set("nodes", nodesJSON)
154+
155+
return nil
156+
}
157+
158+
func resourceSumologicCsoarPlaybookCreate(d *schema.ResourceData, meta interface{}) error {
159+
return fmt.Errorf("playbooks cannot be created via Terraform. Please create the playbook in the CSOAR UI, export it as JSON, and then import it using 'terraform import'")
160+
}
161+
162+
func resourceSumologicCsoarPlaybookRead(d *schema.ResourceData, meta interface{}) error {
163+
if d.Id() == "" {
164+
return fmt.Errorf("resource ID is empty")
165+
}
166+
167+
// For import-only resources, ensure the name matches the ID
168+
d.Set("name", d.Id())
169+
170+
// For import-only resources, preserve existing state values or set reasonable defaults
171+
// This prevents Terraform from thinking the resource doesn't exist
172+
if d.Get("description") == nil {
173+
d.Set("description", "")
174+
}
175+
if d.Get("tags") == nil {
176+
d.Set("tags", "")
177+
}
178+
if d.Get("is_deleted") == nil {
179+
d.Set("is_deleted", false)
180+
}
181+
if d.Get("draft") == nil {
182+
d.Set("draft", false)
183+
}
184+
if d.Get("is_published") == nil {
185+
d.Set("is_published", true)
186+
}
187+
if d.Get("nested") == nil {
188+
d.Set("nested", false)
189+
}
190+
if d.Get("type") == nil {
191+
d.Set("type", "General")
192+
}
193+
if d.Get("is_enabled") == nil {
194+
d.Set("is_enabled", true)
195+
}
196+
if d.Get("nodes") == nil {
197+
d.Set("nodes", "[]")
198+
}
199+
if d.Get("links") == nil {
200+
d.Set("links", "[]")
201+
}
202+
203+
return nil
204+
}
205+
206+
func resourceSumologicCsoarPlaybookImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
207+
playbookName := d.Id()
208+
209+
if playbookName == "" {
210+
return nil, fmt.Errorf("import ID (playbook name) cannot be empty")
211+
}
212+
213+
d.SetId(playbookName)
214+
d.Set("name", playbookName)
215+
216+
// Set default values for all schema fields to prevent Terraform from thinking this is a new resource
217+
d.Set("description", "")
218+
d.Set("tags", "")
219+
d.Set("is_deleted", false)
220+
d.Set("draft", false)
221+
d.Set("is_published", true)
222+
d.Set("nested", false)
223+
d.Set("type", "General")
224+
d.Set("is_enabled", true)
225+
d.Set("nodes", "[]")
226+
d.Set("links", "[]")
227+
228+
return []*schema.ResourceData{d}, nil
229+
}

0 commit comments

Comments
 (0)