Skip to content

Commit 7e52661

Browse files
committed
feat: add aws_ec2_fleet table
1 parent 5638170 commit 7e52661

File tree

3 files changed

+626
-0
lines changed

3 files changed

+626
-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: 347 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,347 @@
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+
},
115+
{
116+
Name: "valid_until",
117+
Description: "The end date and time of the request.",
118+
Type: proto.ColumnType_TIMESTAMP,
119+
},
120+
{
121+
Name: "target_capacity_specification",
122+
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.",
123+
Type: proto.ColumnType_JSON,
124+
},
125+
{
126+
Name: "spot_options",
127+
Description: "The configuration of Spot Instances in an EC2 Fleet.",
128+
Type: proto.ColumnType_JSON,
129+
},
130+
{
131+
Name: "on_demand_options",
132+
Description: "The allocation strategy of On-Demand Instances in an EC2 Fleet.",
133+
Type: proto.ColumnType_JSON,
134+
},
135+
{
136+
Name: "launch_template_configs",
137+
Description: "The launch template and overrides.",
138+
Type: proto.ColumnType_JSON,
139+
},
140+
{
141+
Name: "instances",
142+
Description: "Information about the instances that were launched by the fleet.",
143+
Type: proto.ColumnType_JSON,
144+
},
145+
{
146+
Name: "errors",
147+
Description: "Information about the instances that could not be launched by the fleet.",
148+
Type: proto.ColumnType_JSON,
149+
},
150+
{
151+
Name: "tags_src",
152+
Description: "A list of tags assigned to the EC2 Fleet.",
153+
Type: proto.ColumnType_JSON,
154+
Transform: transform.FromField("Tags"),
155+
},
156+
157+
// Steampipe standard columns
158+
{
159+
Name: "title",
160+
Description: resourceInterfaceDescription("title"),
161+
Type: proto.ColumnType_STRING,
162+
Transform: transform.FromField("FleetId"),
163+
},
164+
{
165+
Name: "tags",
166+
Description: resourceInterfaceDescription("tags"),
167+
Type: proto.ColumnType_JSON,
168+
Transform: transform.FromField("Tags").Transform(ec2FleetTagsToTurbotTags),
169+
},
170+
{
171+
Name: "akas",
172+
Description: resourceInterfaceDescription("akas"),
173+
Type: proto.ColumnType_JSON,
174+
Hydrate: getEc2FleetARN,
175+
Transform: transform.FromValue().Transform(transform.EnsureStringArray),
176+
},
177+
}),
178+
}
179+
}
180+
181+
//// LIST FUNCTION
182+
183+
func listEc2Fleets(ctx context.Context, d *plugin.QueryData, _ *plugin.HydrateData) (interface{}, error) {
184+
// Create session
185+
svc, err := EC2Client(ctx, d)
186+
if err != nil {
187+
plugin.Logger(ctx).Error("aws_ec2_fleet.listEc2Fleets", "connection_error", err)
188+
return nil, err
189+
}
190+
191+
// Limiting the results
192+
maxLimit := int32(1000)
193+
if d.QueryContext.Limit != nil {
194+
limit := int32(*d.QueryContext.Limit)
195+
if limit < maxLimit {
196+
maxLimit = limit
197+
}
198+
}
199+
200+
input := &ec2.DescribeFleetsInput{
201+
MaxResults: aws.Int32(maxLimit),
202+
}
203+
204+
// Build filters
205+
filters := buildEc2FleetFilter(d.Quals)
206+
if len(filters) > 0 {
207+
input.Filters = filters
208+
}
209+
210+
paginator := ec2.NewDescribeFleetsPaginator(svc, input, func(o *ec2.DescribeFleetsPaginatorOptions) {
211+
o.Limit = maxLimit
212+
})
213+
214+
for paginator.HasMorePages() {
215+
output, err := paginator.NextPage(ctx)
216+
if err != nil {
217+
plugin.Logger(ctx).Error("aws_ec2_fleet.listEc2Fleets", "api_error", err)
218+
return nil, err
219+
}
220+
221+
for _, fleet := range output.Fleets {
222+
d.StreamListItem(ctx, fleet)
223+
224+
// Context can be cancelled due to manual cancellation or the limit has been hit
225+
if d.RowsRemaining(ctx) == 0 {
226+
return nil, nil
227+
}
228+
}
229+
}
230+
231+
return nil, nil
232+
}
233+
234+
//// HYDRATE FUNCTIONS
235+
236+
func getEc2Fleet(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
237+
fleetId := d.EqualsQualString("fleet_id")
238+
239+
// Empty check
240+
if fleetId == "" {
241+
return nil, nil
242+
}
243+
244+
// Create session
245+
svc, err := EC2Client(ctx, d)
246+
if err != nil {
247+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2Fleet", "connection_error", err)
248+
return nil, err
249+
}
250+
251+
params := &ec2.DescribeFleetsInput{
252+
FleetIds: []string{fleetId},
253+
}
254+
255+
op, err := svc.DescribeFleets(ctx, params)
256+
if err != nil {
257+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2Fleet", "api_error", err)
258+
return nil, err
259+
}
260+
261+
if len(op.Fleets) > 0 {
262+
return op.Fleets[0], nil
263+
}
264+
265+
return nil, nil
266+
}
267+
268+
func getEc2FleetARN(ctx context.Context, d *plugin.QueryData, h *plugin.HydrateData) (interface{}, error) {
269+
region := d.EqualsQualString(matrixKeyRegion)
270+
fleet := h.Item.(types.FleetData)
271+
272+
commonData, err := getCommonColumns(ctx, d, h)
273+
if err != nil {
274+
plugin.Logger(ctx).Error("aws_ec2_fleet.getEc2FleetARN", "common_data_error", err)
275+
return nil, err
276+
}
277+
commonColumnData := commonData.(*awsCommonColumnData)
278+
279+
// Build ARN
280+
// arn:${Partition}:ec2:${Region}:${Account}:fleet/${FleetId}
281+
// https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazonec2.html#amazonec2-fleet
282+
arn := "arn:" + commonColumnData.Partition + ":ec2:" + region + ":" + commonColumnData.AccountId + ":fleet/" + *fleet.FleetId
283+
284+
return arn, nil
285+
}
286+
287+
//// TRANSFORM FUNCTIONS
288+
289+
func ec2FleetTagsToTurbotTags(ctx context.Context, d *transform.TransformData) (interface{}, error) {
290+
fleet := d.HydrateItem.(types.FleetData)
291+
292+
if fleet.Tags == nil {
293+
return nil, nil
294+
}
295+
296+
turbotTags := make(map[string]string)
297+
for _, tag := range fleet.Tags {
298+
if tag.Key != nil && tag.Value != nil {
299+
turbotTags[*tag.Key] = *tag.Value
300+
}
301+
}
302+
return turbotTags, nil
303+
}
304+
305+
//// UTILITY FUNCTIONS
306+
307+
func buildEc2FleetFilter(quals plugin.KeyColumnQualMap) []types.Filter {
308+
filters := []types.Filter{}
309+
310+
filterQuals := map[string]string{
311+
"fleet_state": "fleet-state",
312+
"activity_status": "activity-status",
313+
"type": "type",
314+
"excess_capacity_termination_policy": "excess-capacity-termination-policy",
315+
}
316+
317+
for columnName, filterName := range filterQuals {
318+
if quals[columnName] != nil {
319+
filter := types.Filter{
320+
Name: aws.String(filterName),
321+
}
322+
for _, q := range quals[columnName].Quals {
323+
value := q.Value.GetStringValue()
324+
if value != "" {
325+
filter.Values = append(filter.Values, value)
326+
}
327+
}
328+
if len(filter.Values) > 0 {
329+
filters = append(filters, filter)
330+
}
331+
}
332+
}
333+
334+
// Handle boolean filter for replace_unhealthy_instances
335+
if quals["replace_unhealthy_instances"] != nil {
336+
for _, q := range quals["replace_unhealthy_instances"].Quals {
337+
value := q.Value.GetBoolValue()
338+
filter := types.Filter{
339+
Name: aws.String("replace-unhealthy-instances"),
340+
Values: []string{fmt.Sprintf("%t", value)},
341+
}
342+
filters = append(filters, filter)
343+
}
344+
}
345+
346+
return filters
347+
}

0 commit comments

Comments
 (0)