Skip to content

Commit 385e9c0

Browse files
authored
Merge pull request #683 from SumoLogic/sjain-list-apps
SUMO-245407: Add support for list apps v2 as data resource
2 parents fe20dd5 + 6ed1b1d commit 385e9c0

File tree

5 files changed

+372
-0
lines changed

5 files changed

+372
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
## X.Y.Z (Unreleased)
22
* Add new change notes here
33

4+
FEATURES:
5+
* **New Data Source:** sumologic_apps
6+
47
ENHANCEMENTS:
58
* Improve error message when an API URL is constructed with missing parameters
69

Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
package sumologic
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/hex"
6+
"encoding/json"
7+
"fmt"
8+
"net/url"
9+
"sort"
10+
"strings"
11+
12+
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
13+
)
14+
15+
func dataSourceSumoLogicApps() *schema.Resource {
16+
return &schema.Resource{
17+
Read: dataSourceSumoLogicAppsRead,
18+
Schema: map[string]*schema.Schema{
19+
"name": {
20+
Type: schema.TypeString,
21+
Optional: true,
22+
},
23+
"author": {
24+
Type: schema.TypeString,
25+
Optional: true,
26+
},
27+
"id": {
28+
Type: schema.TypeString,
29+
Computed: true,
30+
},
31+
"apps": {
32+
Type: schema.TypeList,
33+
Computed: true,
34+
Elem: &schema.Resource{
35+
Schema: map[string]*schema.Schema{
36+
"uuid": {Type: schema.TypeString, Computed: true},
37+
"name": {Type: schema.TypeString, Computed: true},
38+
"description": {Type: schema.TypeString, Computed: true},
39+
"latest_version": {Type: schema.TypeString, Computed: true},
40+
"icon": {Type: schema.TypeString, Computed: true},
41+
"author": {Type: schema.TypeString, Computed: true},
42+
"account_types": {
43+
Type: schema.TypeList,
44+
Computed: true,
45+
Elem: &schema.Schema{Type: schema.TypeString},
46+
},
47+
"beta": {Type: schema.TypeBool, Computed: true},
48+
"installs": {Type: schema.TypeInt, Computed: true},
49+
"app_type": {Type: schema.TypeString, Computed: true},
50+
"attributes": {
51+
Type: schema.TypeList,
52+
MaxItems: 1,
53+
Optional: true,
54+
Elem: &schema.Resource{
55+
Schema: map[string]*schema.Schema{
56+
"category": {
57+
Type: schema.TypeList,
58+
Optional: true,
59+
Elem: &schema.Schema{
60+
Type: schema.TypeString,
61+
},
62+
},
63+
"use_case": {
64+
Type: schema.TypeList,
65+
Optional: true,
66+
Elem: &schema.Schema{
67+
Type: schema.TypeString,
68+
},
69+
},
70+
"collection": {
71+
Type: schema.TypeList,
72+
Optional: true,
73+
Elem: &schema.Schema{
74+
Type: schema.TypeString,
75+
},
76+
},
77+
},
78+
},
79+
},
80+
"family": {Type: schema.TypeString, Computed: true},
81+
"installable": {Type: schema.TypeBool, Computed: true},
82+
"show_on_marketplace": {Type: schema.TypeBool, Computed: true},
83+
},
84+
},
85+
},
86+
},
87+
}
88+
}
89+
90+
func dataSourceSumoLogicAppsRead(d *schema.ResourceData, meta interface{}) error {
91+
c := meta.(*Client)
92+
93+
// Read apps from the API
94+
id, apps, err := c.getApps(d.Get("name").(string), d.Get("author").(string))
95+
if err != nil {
96+
return err
97+
}
98+
99+
if err := d.Set("apps", flattenApps(apps)); err != nil {
100+
return err
101+
}
102+
103+
d.SetId(id)
104+
105+
return nil
106+
}
107+
108+
func (s *Client) getApps(name string, author string) (string, []App, error) {
109+
// Construct the base URL
110+
baseURL := "v2/apps"
111+
112+
// Create url.Values to hold the query parameters
113+
params := url.Values{}
114+
if name != "" {
115+
params.Add("name", name)
116+
}
117+
if author != "" {
118+
params.Add("author", author)
119+
}
120+
121+
// Construct the full URL string
122+
fullURL := baseURL
123+
if len(params) > 0 {
124+
fullURL += "?" + params.Encode()
125+
}
126+
127+
data, err := s.Get(fullURL)
128+
if err != nil {
129+
return "", nil, err
130+
}
131+
132+
apps := AppsResponse{}
133+
err = json.Unmarshal(data, &apps)
134+
135+
if err != nil {
136+
return "", nil, err
137+
}
138+
139+
// Generate a unique ID for this data source
140+
id := generateDataSourceId(name, author, apps.Apps)
141+
142+
return id, apps.Apps, nil
143+
}
144+
145+
func generateDataSourceId(name string, author string, apps []App) string {
146+
// Start with the filter parameters
147+
idParts := []string{
148+
fmt.Sprintf("name:%s", name),
149+
fmt.Sprintf("author:%s", author),
150+
}
151+
152+
// Add a sorted list of app UUIDs
153+
var uuids []string
154+
for _, app := range apps {
155+
uuids = append(uuids, app.UUID)
156+
}
157+
sort.Strings(uuids)
158+
idParts = append(idParts, fmt.Sprintf("apps:%s", strings.Join(uuids, ",")))
159+
160+
// Join all parts and create a hash
161+
idString := strings.Join(idParts, "|")
162+
hash := sha256.Sum256([]byte(idString))
163+
return hex.EncodeToString(hash[:])
164+
}
165+
166+
func flattenApps(apps []App) []interface{} {
167+
var flattenedApps []interface{}
168+
for _, app := range apps {
169+
170+
internalAttributes := make(map[string]interface{})
171+
internalAttributes["category"] = app.Attributes.Category
172+
internalAttributes["use_case"] = app.Attributes.UseCase
173+
internalAttributes["collection"] = app.Attributes.Collection
174+
attributes := []interface{}{
175+
internalAttributes,
176+
}
177+
178+
flattenedApp := map[string]interface{}{
179+
"uuid": app.UUID,
180+
"name": app.Name,
181+
"description": app.Description,
182+
"latest_version": app.LatestVersion,
183+
"icon": app.Icon,
184+
"author": app.Author,
185+
"account_types": app.AccountTypes,
186+
"beta": app.Beta,
187+
"installs": app.Installs,
188+
"app_type": app.AppType,
189+
"attributes": attributes,
190+
"family": app.Family,
191+
"installable": app.Installable,
192+
"show_on_marketplace": app.ShowOnMarketplace,
193+
}
194+
flattenedApps = append(flattenedApps, flattenedApp)
195+
}
196+
return flattenedApps
197+
}
198+
199+
type AppsResponse struct {
200+
Apps []App `json:"apps"`
201+
}
202+
203+
type App struct {
204+
UUID string `json:"uuid"`
205+
Name string `json:"name"`
206+
Description string `json:"description"`
207+
LatestVersion string `json:"latestVersion"`
208+
Icon string `json:"icon"`
209+
Author string `json:"author"`
210+
AccountTypes []string `json:"accountTypes"`
211+
Beta bool `json:"beta"`
212+
Installs int `json:"installs"`
213+
AppType string `json:"appType"`
214+
Attributes struct {
215+
Category []string `json:"category"`
216+
UseCase []string `json:"useCase"`
217+
Collection []string `json:"collection"`
218+
} `json:"attributes"`
219+
Family string `json:"family"`
220+
Installable bool `json:"installable"`
221+
ShowOnMarketplace bool `json:"showOnMarketplace"`
222+
}
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
package sumologic
2+
3+
import (
4+
"fmt"
5+
"strconv"
6+
"testing"
7+
8+
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
9+
"github.com/hashicorp/terraform-plugin-sdk/terraform"
10+
)
11+
12+
func TestAccDataSourceSumoLogicApps_basic(t *testing.T) {
13+
resource.Test(t, resource.TestCase{
14+
PreCheck: func() { testAccPreCheck(t) },
15+
Providers: testAccProviders,
16+
Steps: []resource.TestStep{
17+
{
18+
Config: testAccDataSourceSumoLogicAppsConfig_basic,
19+
Check: resource.ComposeTestCheckFunc(
20+
testAccCheckSumoLogicAppsDataSourceID("data.sumologic_apps.test"),
21+
resource.TestCheckResourceAttrSet("data.sumologic_apps.test", "apps.#"),
22+
checkResourceAttrGreaterThanZero("data.sumologic_apps.test", "apps.#"),
23+
),
24+
},
25+
},
26+
})
27+
}
28+
29+
func TestAccDataSourceSumoLogicApps_filtered(t *testing.T) {
30+
resource.Test(t, resource.TestCase{
31+
PreCheck: func() { testAccPreCheck(t) },
32+
Providers: testAccProviders,
33+
Steps: []resource.TestStep{
34+
{
35+
Config: testAccDataSourceSumoLogicAppsConfig_filtered,
36+
Check: resource.ComposeTestCheckFunc(
37+
testAccCheckSumoLogicAppsDataSourceID("data.sumologic_apps.filtered"),
38+
resource.TestCheckResourceAttr("data.sumologic_apps.filtered", "apps.#", "1"),
39+
resource.TestCheckResourceAttr("data.sumologic_apps.filtered", "apps.0.name", "MySQL - OpenTelemetry"),
40+
resource.TestCheckResourceAttr("data.sumologic_apps.filtered", "apps.0.author", "Sumo Logic"),
41+
),
42+
},
43+
},
44+
})
45+
}
46+
47+
func testAccCheckSumoLogicAppsDataSourceID(n string) resource.TestCheckFunc {
48+
return func(s *terraform.State) error {
49+
rs, ok := s.RootModule().Resources[n]
50+
if !ok {
51+
return fmt.Errorf("Can't find SumoLogic Apps data source: %s", n)
52+
}
53+
54+
if rs.Primary.ID == "" {
55+
return fmt.Errorf("SumoLogic Apps data source ID not set")
56+
}
57+
return nil
58+
}
59+
}
60+
61+
func checkResourceAttrGreaterThanZero(resourceName, attributeName string) resource.TestCheckFunc {
62+
return func(s *terraform.State) error {
63+
rs, ok := s.RootModule().Resources[resourceName]
64+
if !ok {
65+
return fmt.Errorf("Not found: %s", resourceName)
66+
}
67+
68+
attrValue, ok := rs.Primary.Attributes[attributeName]
69+
if !ok {
70+
return fmt.Errorf("Attribute not found: %s", attributeName)
71+
}
72+
73+
// Convert the attribute value to an integer
74+
count, err := strconv.Atoi(attrValue)
75+
if err != nil {
76+
return fmt.Errorf("Error converting attribute value to integer: %s", err)
77+
}
78+
79+
// Check if count is greater than 0
80+
if count <= 0 {
81+
return fmt.Errorf("Expected %s to be greater than 0, got %d", attributeName, count)
82+
}
83+
84+
return nil
85+
}
86+
}
87+
88+
const testAccDataSourceSumoLogicAppsConfig_basic = `
89+
data "sumologic_apps" "test" {}
90+
`
91+
92+
const testAccDataSourceSumoLogicAppsConfig_filtered = `
93+
data "sumologic_apps" "filtered" {
94+
name = "MySQL - OpenTelemetry"
95+
author = "Sumo Logic"
96+
}
97+
`

sumologic/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ func Provider() terraform.ResourceProvider {
137137
"sumologic_role": dataSourceSumologicRole(),
138138
"sumologic_role_v2": dataSourceSumologicRoleV2(),
139139
"sumologic_user": dataSourceSumologicUser(),
140+
"sumologic_apps": dataSourceSumoLogicApps(),
140141
},
141142
ConfigureFunc: providerConfigure,
142143
}

website/docs/d/apps.html.markdown

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
---
2+
layout: "sumologic"
3+
page_title: "SumoLogic: sumologic_apps"
4+
description: |-
5+
Provides an easy way to retrieve all Sumo Logic v2 apps.
6+
---
7+
8+
# sumologic_apps
9+
Provides an easy way to retrieve all Sumo Logic v2 apps.
10+
11+
12+
## Example Usage
13+
```hcl
14+
data "sumologic_apps" "test" {}
15+
```
16+
17+
```hcl
18+
data "sumologic_apps" "test" {
19+
name = "MySQL - OpenTelemetry"
20+
author = "Sumo Logic"
21+
}
22+
```
23+
24+
25+
## Attributes reference
26+
27+
The following attributes are exported:
28+
29+
- `uuid` - UUID of the app.
30+
- `name` - Name of the app.
31+
- `description` - Description of the app.
32+
- `latest_version` - Latest version of the app.
33+
- `icon` - URL of the icon for the app.
34+
- `author` - Author of the app.
35+
- `account_types` - URL of the icon for the app
36+
- `log_analytics_filter` - The search filter which would be applied on partitions which belong to Log Analytics product area.
37+
- `beta` - URL of the icon for the app.
38+
- `installs` - Number of times the app was installed.
39+
- `appType` - Type of an app.
40+
- `attributes` - A map of attributes for this app. Attributes allow to group apps based on different criteria.
41+
### Values in attributes type are :
42+
- `category`
43+
- `use_case`
44+
- `collection`
45+
- `family` - Provides a mechanism to link different apps.
46+
- `installable` - Whether the app is installable or not as not all apps are installable.
47+
- `show_on_marketplace` - Whether the app should show up on sumologic.com/applications webpage.
48+
49+

0 commit comments

Comments
 (0)