Skip to content

Commit 14bbe6c

Browse files
committed
feat: add aws_ec2_fleet table
1 parent 5638170 commit 14bbe6c

File tree

3 files changed

+628
-0
lines changed

3 files changed

+628
-0
lines changed

aws/plugin.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ func Plugin(ctx context.Context) *plugin.Plugin {
392392
"aws_ec2_capacity_reservation": tableAwsEc2CapacityReservation(ctx),
393393
"aws_ec2_classic_load_balancer": tableAwsEc2ClassicLoadBalancer(ctx),
394394
"aws_ec2_client_vpn_endpoint": tableAwsEC2ClientVPNEndpoint(ctx),
395+
"aws_ec2_fleet": tableAwsEc2Fleet(ctx),
395396
"aws_ec2_gateway_load_balancer": tableAwsEc2GatewayLoadBalancer(ctx),
396397
"aws_ec2_instance_availability": tableAwsInstanceAvailability(ctx),
397398
"aws_ec2_instance_metric_cpu_utilization_daily": tableAwsEc2InstanceMetricCpuUtilizationDaily(ctx),

aws/table_aws_ec2_fleet.go

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
package aws
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"github.com/aws/aws-sdk-go-v2/aws"
8+
"github.com/aws/aws-sdk-go-v2/service/ec2"
9+
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
10+
11+
"github.com/turbot/steampipe-plugin-sdk/v5/grpc/proto"
12+
"github.com/turbot/steampipe-plugin-sdk/v5/plugin"
13+
"github.com/turbot/steampipe-plugin-sdk/v5/plugin/transform"
14+
)
15+
16+
//// TABLE DEFINITION
17+
18+
func tableAwsEc2Fleet(_ context.Context) *plugin.Table {
19+
return &plugin.Table{
20+
Name: "aws_ec2_fleet",
21+
Description: "AWS EC2 Fleet",
22+
Get: &plugin.GetConfig{
23+
KeyColumns: plugin.SingleColumn("fleet_id"),
24+
IgnoreConfig: &plugin.IgnoreConfig{
25+
ShouldIgnoreErrorFunc: shouldIgnoreErrors([]string{"InvalidFleetId.NotFound", "InvalidFleetId.Malformed"}),
26+
},
27+
Hydrate: getEc2Fleet,
28+
Tags: map[string]string{"service": "ec2", "action": "DescribeFleets"},
29+
},
30+
List: &plugin.ListConfig{
31+
Hydrate: listEc2Fleets,
32+
Tags: map[string]string{"service": "ec2", "action": "DescribeFleets"},
33+
KeyColumns: []*plugin.KeyColumn{
34+
{Name: "fleet_state", Require: plugin.Optional},
35+
{Name: "activity_status", Require: plugin.Optional},
36+
{Name: "type", Require: plugin.Optional},
37+
{Name: "excess_capacity_termination_policy", Require: plugin.Optional},
38+
{Name: "replace_unhealthy_instances", Require: plugin.Optional, Operators: []string{"=", "<>"}},
39+
},
40+
},
41+
GetMatrixItemFunc: SupportedRegionMatrix(AWS_EC2_SERVICE_ID),
42+
Columns: awsRegionalColumns([]*plugin.Column{
43+
{
44+
Name: "fleet_id",
45+
Description: "The ID of the EC2 Fleet.",
46+
Type: proto.ColumnType_STRING,
47+
},
48+
{
49+
Name: "arn",
50+
Description: "The Amazon Resource Name (ARN) of the EC2 Fleet.",
51+
Type: proto.ColumnType_STRING,
52+
Hydrate: getEc2FleetARN,
53+
Transform: transform.FromValue(),
54+
},
55+
{
56+
Name: "fleet_state",
57+
Description: "The state of the EC2 Fleet.",
58+
Type: proto.ColumnType_STRING,
59+
},
60+
{
61+
Name: "activity_status",
62+
Description: "The progress of the EC2 Fleet.",
63+
Type: proto.ColumnType_STRING,
64+
},
65+
{
66+
Name: "create_time",
67+
Description: "The creation date and time of the EC2 Fleet.",
68+
Type: proto.ColumnType_TIMESTAMP,
69+
},
70+
{
71+
Name: "type",
72+
Description: "The type of request. Indicates whether the EC2 Fleet only requests the target capacity or also attempts to maintain it.",
73+
Type: proto.ColumnType_STRING,
74+
},
75+
{
76+
Name: "client_token",
77+
Description: "Unique, case-sensitive identifier that you provide to ensure the idempotency of the request.",
78+
Type: proto.ColumnType_STRING,
79+
},
80+
{
81+
Name: "context",
82+
Description: "Reserved.",
83+
Type: proto.ColumnType_STRING,
84+
},
85+
{
86+
Name: "excess_capacity_termination_policy",
87+
Description: "Indicates whether running instances should be terminated if the target capacity of the EC2 Fleet is decreased below the current size of the EC2 Fleet.",
88+
Type: proto.ColumnType_STRING,
89+
},
90+
{
91+
Name: "fulfilled_capacity",
92+
Description: "The number of units fulfilled by this request compared to the set target capacity.",
93+
Type: proto.ColumnType_DOUBLE,
94+
},
95+
{
96+
Name: "fulfilled_on_demand_capacity",
97+
Description: "The number of units fulfilled by this request compared to the set target On-Demand capacity.",
98+
Type: proto.ColumnType_DOUBLE,
99+
},
100+
{
101+
Name: "replace_unhealthy_instances",
102+
Description: "Indicates whether EC2 Fleet should replace unhealthy Spot Instances.",
103+
Type: proto.ColumnType_BOOL,
104+
},
105+
{
106+
Name: "terminate_instances_with_expiration",
107+
Description: "Indicates whether running instances should be terminated when the EC2 Fleet expires.",
108+
Type: proto.ColumnType_BOOL,
109+
},
110+
{
111+
Name: "valid_from",
112+
Description: "The start date and time of the request.",
113+
Type: proto.ColumnType_TIMESTAMP,
114+
Transform: transform.FromField("ValidFrom").Transform(transform.NullIfZeroValue),
115+
},
116+
{
117+
Name: "valid_until",
118+
Description: "The end date and time of the request.",
119+
Type: proto.ColumnType_TIMESTAMP,
120+
Transform: transform.FromField("ValidUntil").Transform(transform.NullIfZeroValue),
121+
},
122+
{
123+
Name: "target_capacity_specification",
124+
Description: "The number of units to request. You can choose to set the target capacity in terms of instances or a performance characteristic that is important to your application workload.",
125+
Type: proto.ColumnType_JSON,
126+
},
127+
{
128+
Name: "spot_options",
129+
Description: "The configuration of Spot Instances in an EC2 Fleet.",
130+
Type: proto.ColumnType_JSON,
131+
},
132+
{
133+
Name: "on_demand_options",
134+
Description: "The allocation strategy of On-Demand Instances in an EC2 Fleet.",
135+
Type: proto.ColumnType_JSON,
136+
},
137+
{
138+
Name: "launch_template_configs",
139+
Description: "The launch template and overrides.",
140+
Type: proto.ColumnType_JSON,
141+
},
142+
{
143+
Name: "instances",
144+
Description: "Information about the instances that were launched by the fleet.",
145+
Type: proto.ColumnType_JSON,
146+
},
147+
{
148+
Name: "errors",
149+
Description: "Information about the instances that could not be launched by the fleet.",
150+
Type: proto.ColumnType_JSON,
151+
},
152+
{
153+
Name: "tags_src",
154+
Description: "A list of tags assigned to the EC2 Fleet.",
155+
Type: proto.ColumnType_JSON,
156+
Transform: transform.FromField("Tags"),
157+
},
158+
159+
// Steampipe standard columns
160+
{
161+
Name: "title",
162+
Description: resourceInterfaceDescription("title"),
163+
Type: proto.ColumnType_STRING,
164+
Transform: transform.FromField("FleetId"),
165+
},
166+
{
167+
Name: "tags",
168+
Description: resourceInterfaceDescription("tags"),
169+
Type: proto.ColumnType_JSON,
170+
Transform: transform.FromField("Tags").Transform(ec2FleetTagsToTurbotTags),
171+
},
172+
{
173+
Name: "akas",
174+
Description: resourceInterfaceDescription("akas"),
175+
Type: proto.ColumnType_JSON,
176+
Hydrate: getEc2FleetARN,
177+
Transform: transform.FromValue().Transform(transform.EnsureStringArray),
178+
},
179+
}),
180+
}
181+
}
182+
183+
//// LIST FUNCTION
184+
185+
func listEc2Fleets(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
186+
// Create session
187+
svc, err := EC2Client(ctx, d)
188+
if err != nil {
189+
plugin.Logger(ctx).Error("aws_ec2_fleet.listEc2Fleets", "connection_error", err)
190+
return nil, err
191+
}
192+
193+
// Limiting the results
194+
maxLimit := int32(1000)
195+
if d.QueryContext.Limit != nil {
196+
limit := int32(*d.QueryContext.Limit)
197+
if limit < maxLimit {
198+
maxLimit = limit
199+
}
200+
}
201+
202+
input := &ec2.DescribeFleetsInput{
203+
MaxResults: aws.Int32(maxLimit),
204+
}
205+
206+
// Build filters
207+
filters := buildEc2FleetFilter(d.Quals)
208+
if len(filters) > 0 {
209+
input.Filters = filters
210+
}
211+
212+
paginator := ec2.NewDescribeFleetsPaginator(svc, input, func(o *ec2.DescribeFleetsPaginatorOptions) {
213+
o.Limit = maxLimit
214+
})
215+
216+
for paginator.HasMorePages() {
217+
output, err := paginator.NextPage(ctx)
218+
if err != nil {
219+
plugin.Logger(ctx).Error("aws_ec2_fleet.listEc2Fleets", "api_error", err)
220+
return nil, err
221+
}
222+
223+
for _, fleet := range output.Fleets {
224+
d.StreamListItem(ctx, fleet)
225+
226+
// Context can be cancelled due to manual cancellation or the limit has been hit
227+
if d.RowsRemaining(ctx) == 0 {
228+
return nil, nil
229+
}
230+
}
231+
}
232+
233+
return nil, nil
234+
}
235+
236+
//// HYDRATE FUNCTIONS
237+
238+
func getEc2Fleet(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
239+
fleetId := d.EqualsQualString("fleet_id")
240+
241+
// Empty check
242+
if fleetId == "" {
243+
return nil, nil
244+
}
245+
246+
// Create session
247+
svc, err := EC2Client(ctx, d)
248+
if err != nil {
249+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2Fleet", "connection_error", err)
250+
return nil, err
251+
}
252+
253+
params := &ec2.DescribeFleetsInput{
254+
FleetIds: []string{fleetId},
255+
}
256+
257+
op, err := svc.DescribeFleets(ctx, params)
258+
if err != nil {
259+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2Fleet", "api_error", err)
260+
return nil, err
261+
}
262+
263+
if len(op.Fleets) > 0 {
264+
return op.Fleets[0], nil
265+
}
266+
267+
return nil, nil
268+
}
269+
270+
func getEc2FleetARN(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
271+
region := d.EqualsQualString(matrixKeyRegion)
272+
fleet := h.Item.(types.FleetData)
273+
274+
commonData, err := getCommonColumns(ctx, d, h)
275+
if err != nil {
276+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2FleetARN", "common_data_error", err)
277+
return nil, err
278+
}
279+
commonColumnData := commonData.(*awsCommonColumnData)
280+
281+
// Build ARN
282+
// arn:${Partition}:ec2:${Region}:${Account}:fleet/${FleetId}
283+
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html#amazonec2-fleet
284+
arn := "arn:" + commonColumnData.Partition + ":ec2:" + region + ":" + commonColumnData.AccountId + ":fleet/" + *fleet.FleetId
285+
286+
return arn, nil
287+
}
288+
289+
//// TRANSFORM FUNCTIONS
290+
291+
func ec2FleetTagsToTurbotTags(ctx context.Context, d *transform.TransformData) (interface{}, error) {
292+
fleet := d.HydrateItem.(types.FleetData)
293+
294+
if fleet.Tags == nil {
295+
return nil, nil
296+
}
297+
298+
turbotTags := make(map[string]string)
299+
for _, tag := range fleet.Tags {
300+
if tag.Key != nil && tag.Value != nil {
301+
turbotTags[*tag.Key] = *tag.Value
302+
}
303+
}
304+
return turbotTags, nil
305+
}
306+
307+
//// UTILITY FUNCTIONS
308+
309+
func buildEc2FleetFilter(quals plugin.KeyColumnQualMap) []types.Filter {
310+
filters := []types.Filter{}
311+
312+
filterQuals := map[string]string{
313+
"fleet_state": "fleet-state",
314+
"activity_status": "activity-status",
315+
"type": "type",
316+
"excess_capacity_termination_policy": "excess-capacity-termination-policy",
317+
}
318+
319+
for columnName, filterName := range filterQuals {
320+
if quals[columnName] != nil {
321+
filter := types.Filter{
322+
Name: aws.String(filterName),
323+
}
324+
for _, q := range quals[columnName].Quals {
325+
value := q.Value.GetStringValue()
326+
if value != "" {
327+
filter.Values = append(filter.Values, value)
328+
}
329+
}
330+
if len(filter.Values) > 0 {
331+
filters = append(filters, filter)
332+
}
333+
}
334+
}
335+
336+
// Handle boolean filter for replace_unhealthy_instances
337+
if quals["replace_unhealthy_instances"] != nil {
338+
for _, q := range quals["replace_unhealthy_instances"].Quals {
339+
value := q.Value.GetBoolValue()
340+
filter := types.Filter{
341+
Name: aws.String("replace-unhealthy-instances"),
342+
Values: []string{fmt.Sprintf("%t", value)},
343+
}
344+
filters = append(filters, filter)
345+
}
346+
}
347+
348+
return filters
349+
}

0 commit comments

Comments
 (0)