• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Smirl / steampipe-plugin-cortex / 18134130753

30 Sep 2025 02:54PM UTC coverage: 74.4% (+1.2%) from 73.233%
18134130753

push

github

web-flow
Merge pull request #38 from mungodewar/extend-group

feat(entity): add single group filter and docs

31 of 39 new or added lines in 1 file covered. (79.49%)

1 existing line in 1 file now uncovered.

372 of 500 relevant lines covered (74.4%)

0.82 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

71.33
/cortex/table_cortex_entity.go
1
package cortex
2

3
import (
4
        "context"
5
        "fmt"
6
        "strconv"
7
        "strings"
8

9
        "github.com/imroc/req/v3"
10
        "github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto"
11
        "github.com/turbot/steampipe-plugin-sdk/v5/plugin"
12
        "github.com/turbot/steampipe-plugin-sdk/v5/plugin/quals"
13
        "github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
14
)
15

16
type ScalarOrMap struct {
17
        Scalar interface{}
18
        Map    map[string]interface{}
19
}
20

21
func (s *ScalarOrMap) UnmarshalYAML(unmarshal func(interface{}) error) error {
×
22
        if err := unmarshal(&s.Map); err == nil {
×
23
                return nil
×
24
        }
×
25
        if err := unmarshal(&s.Scalar); err != nil {
×
26
                return err
×
27
        }
×
28
        return nil
×
29
}
30

31
func (s *ScalarOrMap) Value() interface{} {
×
32
        if s.Scalar != nil {
×
33
                return s.Scalar
×
34
        }
×
35
        return s.Map
×
36
}
37

38
type CortexEntityResponse struct {
39
        Entities   []CortexEntityElement `yaml:"entities"`
40
        Page       int                   `yaml:"page"`
41
        TotalPages int                   `yaml:"totalPages"`
42
        Total      int                   `yaml:"total"`
43
}
44

45
type CortexEntityElement struct {
46
        Name        string                        `yaml:"name"`
47
        Tag         string                        `yaml:"tag"`
48
        Description string                        `yaml:"description"`
49
        Type        string                        `yaml:"type"`
50
        Hierarchy   CortexEntityElementHierarchy  `yaml:"hierarchy"`
51
        Groups      []string                      `yaml:"groups"`
52
        Metadata    []CortexEntityElementMetadata `yaml:"metadata"`
53
        LastUpdated string                        `yaml:"lastUpdated"`
54
        Links       []CortexLink                  `yaml:"links"`
55
        Archived    bool                          `yaml:"isArchived"`
56
        Git         CortexGithub                  `yaml:"git"`
57
        Slack       []CortexSlackChannel          `yaml:"slackChannels"`
58
        Owners      CortexEntityOwners            `yaml:"owners"`
59
}
60

61
type CortexEntityElementHierarchy struct {
62
        Parents []CortexTag `yaml:"parents"`
63
}
64

65
type CortexEntityElementMetadata struct {
66
        Key   string      `yaml:"key"`
67
        Value ScalarOrMap `yaml:"value"`
68
}
69

70
type CortexEntityOwners struct {
71
        Teams       []CortexEntityOwnersTeam       `yaml:"teams"`
72
        Individuals []CortexEntityOwnersIndividual `yaml:"individuals"`
73
}
74

75
type CortexEntityOwnersTeam struct {
76
        Tag string `yaml:"tag"`
77
}
78

79
type CortexEntityOwnersIndividual struct {
80
        Email string `yaml:"email"`
81
}
82

83
func tableCortexEntity() *plugin.Table {
1✔
84
        return &plugin.Table{
1✔
85
                Name:        "cortex_entity",
1✔
86
                Description: "Cortex list entities api.",
1✔
87
                List: &plugin.ListConfig{
1✔
88
                        Hydrate: listEntitiesHydrator,
1✔
89
                        KeyColumns: []*plugin.KeyColumn{
1✔
90
                                {Name: "archived", Require: plugin.Optional},
1✔
91
                                {Name: "type", Require: plugin.Optional},
1✔
92
                                {Name: "groups", Require: plugin.Optional, Operators: []string{"=", "?", "?|"}},
1✔
93
                        },
1✔
94
                },
1✔
95
                Columns: []*plugin.Column{
1✔
96
                        {Name: "name", Type: proto.ColumnType_STRING, Description: "Pretty name of the entity."},
1✔
97
                        {Name: "tag", Type: proto.ColumnType_STRING, Description: "The x-cortex-tag of the entity."},
1✔
98
                        {Name: "description", Type: proto.ColumnType_STRING, Description: "Description."},
1✔
99
                        {Name: "type", Type: proto.ColumnType_STRING, Description: "Entity Type."},
1✔
100
                        {Name: "parents", Type: proto.ColumnType_JSON, Description: "Parents of the entity.", Transform: FromStructSlice[CortexTag]("Hierarchy.Parents", "Tag")},
1✔
101
                        {Name: "groups", Type: proto.ColumnType_JSON, Description: "Groups, kind of like tags."},
1✔
102
                        {Name: "metadata", Type: proto.ColumnType_JSON, Description: "Raw custom metadata", Transform: transform.FromField("Metadata").Transform(TagArrayToMap)},
1✔
103
                        {Name: "last_updated", Type: proto.ColumnType_TIMESTAMP, Description: "Last updated time."},
1✔
104
                        {Name: "links", Type: proto.ColumnType_JSON, Description: "List of links", Transform: FromStructSlice[CortexLink]("Links", "Url")},
1✔
105
                        {Name: "archived", Type: proto.ColumnType_BOOL, Description: "Is archived."},
1✔
106
                        {Name: "repository", Type: proto.ColumnType_STRING, Description: "Git repo full name", Transform: transform.FromField("Git.Repository")},
1✔
107
                        {Name: "slack_channels", Type: proto.ColumnType_JSON, Description: "List of string slack channels"},
1✔
108
                        {Name: "owner_teams", Type: proto.ColumnType_JSON, Description: "List of owning team tags", Transform: FromStructSlice[CortexEntityOwnersTeam]("Owners.Teams", "Tag")},
1✔
109
                        {Name: "owner_individuals", Type: proto.ColumnType_JSON, Description: "List of owning individuals emails", Transform: FromStructSlice[CortexEntityOwnersIndividual]("Owners.Individuals", "Email")},
1✔
110
                },
1✔
111
        }
1✔
112
}
1✔
113

114
func listEntitiesHydrator(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
×
115
        logger := plugin.Logger(ctx)
×
116
        config := GetConfig(d.Connection)
×
117
        client := CortexHTTPClient(ctx, config)
×
118
        hydratorWriter := QueryDataWriter{d}
×
119

×
120
        // Extract parameters from QueryData
×
121
        archived := "false"
×
122
        if d.EqualsQuals["archived"] != nil && d.EqualsQuals["archived"].GetBoolValue() {
×
123
                logger.Debug("listEntitiesHydrator", "archived", d.EqualsQuals["archived"])
×
124
                archived = "true"
×
125
        }
×
126

127
        types := ""
×
NEW
128
        if d.Quals["type"] != nil {
×
NEW
129
                types = buildListFilter(d.Quals["type"].Quals)
×
NEW
130
        }
×
131

NEW
132
        groups := ""
×
NEW
133
        if d.Quals["groups"] != nil {
×
NEW
134
                groups = buildListFilter(d.Quals["groups"].Quals)
×
UNCOV
135
        }
×
136

NEW
137
        logger.Info("listEntitiesHydrator", "archived", archived, "types", types, "groups", groups)
×
NEW
138
        return nil, listEntities(ctx, client, &hydratorWriter, archived, types, groups)
×
139
}
140

141
func listEntities(ctx context.Context, client *req.Client, writer HydratorWriter, archived string, types string, groups string) error {
1✔
142
        logger := plugin.Logger(ctx)
1✔
143

1✔
144
        var response CortexEntityResponse
1✔
145
        var page int = 0
1✔
146
        for {
2✔
147
                logger.Debug("listEntities", "page", page)
1✔
148
                resp := client.
1✔
149
                        Get("/api/v1/catalog").
1✔
150
                        // Filters
1✔
151
                        SetQueryParam("includeArchived", archived).
1✔
152
                        SetQueryParam("types", types).
1✔
153
                        SetQueryParam("groups", groups).
1✔
154
                        // Options
1✔
155
                        SetQueryParam("yaml", "false").
1✔
156
                        SetQueryParam("includeMetadata", "true").
1✔
157
                        SetQueryParam("includeLinks", "true").
1✔
158
                        SetQueryParam("includeSlackChannels", "true").
1✔
159
                        SetQueryParam("includeOwners", "true").
1✔
160
                        SetQueryParam("includeHierarchyFields", "true").
1✔
161
                        // Pagination
1✔
162
                        SetQueryParam("pageSize", "1000").
1✔
163
                        SetQueryParam("page", strconv.Itoa(page)).
1✔
164
                        Do(ctx)
1✔
165

1✔
166
                // Check for HTTP errors
1✔
167
                if resp.IsErrorState() {
2✔
168
                        logger.Error("listEntities", "Status", resp.Status, "Body", resp.String())
1✔
169
                        return fmt.Errorf("error from cortex API %s: %s", resp.Status, resp.String())
1✔
170
                }
1✔
171

172
                // Unmarshal the response and check for unmarshal errors
173
                err := resp.Into(&response)
1✔
174
                if err != nil {
1✔
175
                        logger.Error("listEntities", "page", page, "Error", err)
×
176
                        return err
×
177
                }
×
178

179
                logger.Debug("listEntities", "totalPages", response.TotalPages, "total", response.Total)
1✔
180

1✔
181
                for _, result := range response.Entities {
2✔
182
                        // send the item to steampipe
1✔
183
                        writer.StreamListItem(ctx, result)
1✔
184
                        // Context can be cancelled due to manual cancellation or the limit has been hit
1✔
185
                        if writer.RowsRemaining(ctx) == 0 {
1✔
186
                                logger.Debug("listEntities", "RowsRemaining", writer.RowsRemaining(ctx))
×
187
                                return nil
×
188
                        }
×
189
                }
190
                page++
1✔
191
                if page >= response.TotalPages {
2✔
192
                        logger.Debug("listEntities", "page", page, "totalPages", response.TotalPages)
1✔
193
                        break
1✔
194
                }
195
        }
196
        return nil
1✔
197
}
198

199
// buildListFilter constructs a comma-separated string of group filters from the provided quals.
200
func buildListFilter(groupQuals []*quals.Qual) string {
1✔
201
        var values []string
1✔
202
        for _, q := range groupQuals {
2✔
203
                switch q.Operator {
1✔
204
                case quals.QualOperatorEqual:
1✔
205
                        // Handle both single string and list of strings
1✔
206
                        if value := q.Value.GetStringValue(); value != "" {
2✔
207
                                values = append(values, value)
1✔
208
                        } else if listValue := q.Value.GetListValue(); listValue != nil {
3✔
209
                                for _, v := range listValue.Values {
2✔
210
                                        if value := v.GetStringValue(); value != "" {
2✔
211
                                                values = append(values, value)
1✔
212
                                        }
1✔
213
                                }
214
                        }
215
                case quals.QualOperatorJsonbExistsOne:
1✔
216
                        if value := q.Value.GetStringValue(); value != "" {
2✔
217
                                values = append(values, value)
1✔
218
                        }
1✔
219
                case quals.QualOperatorJsonbExistsAny:
1✔
220
                        if listValue := q.Value.GetListValue(); listValue != nil {
2✔
221
                                for _, v := range listValue.Values {
2✔
222
                                        if value := v.GetStringValue(); value != "" {
2✔
223
                                                values = append(values, value)
1✔
224
                                        }
1✔
225
                                }
226
                        }
227
                }
228
        }
229
        filter := ""
1✔
230
        if len(values) > 0 {
2✔
231
                filter = strings.Join(values, ",")
1✔
232
        }
1✔
233
        return filter
1✔
234
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc