Skip to content

Commit 5e95b2b

Browse files
committed
add apigateway method adapter
1 parent a9e9e3f commit 5e95b2b

File tree

7 files changed

+357
-3
lines changed

7 files changed

+357
-3
lines changed

adapters/apigateway-method.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
package adapters
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log/slog"
7+
"strings"
8+
9+
"github.com/aws/aws-sdk-go-v2/service/apigateway"
10+
"github.com/overmindtech/aws-source/adapterhelpers"
11+
"github.com/overmindtech/sdp-go"
12+
)
13+
14+
type apigatewayClient interface {
15+
GetMethod(ctx context.Context, params *apigateway.GetMethodInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error)
16+
}
17+
18+
func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodInput) (*sdp.Item, error) {
19+
output, err := client.GetMethod(ctx, input)
20+
if err != nil {
21+
return nil, err
22+
}
23+
24+
attributes, err := adapterhelpers.ToAttributesWithExclude(output, "tags")
25+
if err != nil {
26+
return nil, err
27+
}
28+
29+
// We create a custom ID of {rest-api-id}/{resource-id}/{http-method} e.g.
30+
// rest-api-id/resource-id/GET
31+
methodID := fmt.Sprintf(
32+
"%s/%s/%s",
33+
*input.RestApiId,
34+
*input.ResourceId,
35+
*input.HttpMethod,
36+
)
37+
err = attributes.Set("MethodID", methodID)
38+
if err != nil {
39+
return nil, err
40+
}
41+
42+
item := &sdp.Item{
43+
Type: "apigateway-method",
44+
UniqueAttribute: "MethodID",
45+
Attributes: attributes,
46+
Scope: scope,
47+
}
48+
49+
if output.MethodIntegration != nil {
50+
item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{
51+
Query: &sdp.Query{
52+
Type: "apigateway-integration",
53+
Method: sdp.QueryMethod_GET,
54+
Query: methodID,
55+
Scope: scope,
56+
},
57+
BlastPropagation: &sdp.BlastPropagation{
58+
// They are tightly coupled
59+
In: true,
60+
Out: true,
61+
},
62+
})
63+
}
64+
65+
if output.AuthorizerId != nil {
66+
item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{
67+
Query: &sdp.Query{
68+
Type: "apigateway-authorizer",
69+
Method: sdp.QueryMethod_GET,
70+
Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.AuthorizerId),
71+
Scope: scope,
72+
},
73+
BlastPropagation: &sdp.BlastPropagation{
74+
// Deleting authorizer will affect the method
75+
In: true,
76+
// Deleting method won't affect the authorizer
77+
Out: false,
78+
},
79+
})
80+
}
81+
82+
if output.RequestValidatorId != nil {
83+
item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{
84+
Query: &sdp.Query{
85+
Type: "apigateway-request-validator",
86+
Method: sdp.QueryMethod_GET,
87+
Query: fmt.Sprintf("%s/%s", *input.RestApiId, *output.RequestValidatorId),
88+
Scope: scope,
89+
},
90+
BlastPropagation: &sdp.BlastPropagation{
91+
// Deleting request validator will affect the method
92+
In: true,
93+
// Deleting method won't affect the request validator
94+
Out: false,
95+
},
96+
})
97+
}
98+
99+
return item, nil
100+
}
101+
102+
func NewAPIGatewayMethodAdapter(client apigatewayClient, accountID string, region string) *adapterhelpers.AlwaysGetAdapter[*apigateway.GetMethodInput, *apigateway.GetMethodOutput, *apigateway.GetMethodInput, *apigateway.GetMethodOutput, apigatewayClient, *apigateway.Options] {
103+
return &adapterhelpers.AlwaysGetAdapter[*apigateway.GetMethodInput, *apigateway.GetMethodOutput, *apigateway.GetMethodInput, *apigateway.GetMethodOutput, apigatewayClient, *apigateway.Options]{
104+
ItemType: "apigateway-method",
105+
Client: client,
106+
AccountID: accountID,
107+
Region: region,
108+
AdapterMetadata: apiGatewayMethodAdapterMetadata,
109+
GetFunc: apiGatewayMethodGetFunc,
110+
GetInputMapper: func(scope, query string) *apigateway.GetMethodInput {
111+
// We are using a custom id of {rest-api-id}/{resource-id}/{http-method} e.g.
112+
// rest-api-id/resource-id/GET
113+
f := strings.Split(query, "/")
114+
if len(f) != 3 {
115+
slog.Error(
116+
"query must be in the format of: the rest-api-id/resource-id/http-method",
117+
"found",
118+
query,
119+
)
120+
121+
return nil
122+
}
123+
124+
return &apigateway.GetMethodInput{
125+
RestApiId: &f[0],
126+
ResourceId: &f[1],
127+
HttpMethod: &f[2],
128+
}
129+
},
130+
DisableList: true,
131+
}
132+
}
133+
134+
var apiGatewayMethodAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{
135+
Type: "apigateway-method",
136+
DescriptiveName: "API Gateway Method",
137+
Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK,
138+
SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{
139+
Get: true,
140+
GetDescription: "Get a Method by rest-api id, resource id and http-method",
141+
Search: true,
142+
SearchDescription: "Search Methods by ARN",
143+
},
144+
PotentialLinks: []string{"apigateway-method-response"},
145+
})

adapters/apigateway-method_test.go

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package adapters
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"github.com/overmindtech/sdp-go"
7+
"testing"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/service/apigateway"
12+
"github.com/aws/aws-sdk-go-v2/service/apigateway/types"
13+
"github.com/overmindtech/aws-source/adapterhelpers"
14+
)
15+
16+
type mockAPIGatewayClient struct{}
17+
18+
func (m *mockAPIGatewayClient) GetMethod(ctx context.Context, params *apigateway.GetMethodInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error) {
19+
return &apigateway.GetMethodOutput{
20+
ApiKeyRequired: aws.Bool(false),
21+
HttpMethod: aws.String("GET"),
22+
AuthorizationType: aws.String("NONE"),
23+
AuthorizerId: aws.String("authorizer-id"),
24+
RequestParameters: map[string]bool{},
25+
RequestValidatorId: aws.String("request-validator-id"),
26+
MethodResponses: map[string]types.MethodResponse{
27+
"200": {
28+
ResponseModels: map[string]string{
29+
"application/json": "Empty",
30+
},
31+
StatusCode: aws.String("200"),
32+
},
33+
},
34+
MethodIntegration: &types.Integration{
35+
IntegrationResponses: map[string]types.IntegrationResponse{
36+
"200": {
37+
ResponseTemplates: map[string]string{
38+
"application/json": "",
39+
},
40+
StatusCode: aws.String("200"),
41+
},
42+
},
43+
CacheKeyParameters: []string{},
44+
Uri: aws.String("arn:aws:apigateway:us-west-2:lambda:path/2015-03-31/functions/arn:aws:lambda:us-west-2:123412341234:function:My_Function/invocations"),
45+
HttpMethod: aws.String("POST"),
46+
CacheNamespace: aws.String("y9h6rt"),
47+
Type: "AWS",
48+
},
49+
}, nil
50+
51+
}
52+
53+
func TestApiGatewayGetFunc(t *testing.T) {
54+
ctx := context.Background()
55+
cli := mockAPIGatewayClient{}
56+
57+
input := &apigateway.GetMethodInput{
58+
RestApiId: aws.String("rest-api-id"),
59+
ResourceId: aws.String("resource-id"),
60+
HttpMethod: aws.String("GET"),
61+
}
62+
63+
item, err := apiGatewayMethodGetFunc(ctx, &cli, "scope", input)
64+
if err != nil {
65+
t.Fatalf("unexpected error: %v", err)
66+
}
67+
68+
if err = item.Validate(); err != nil {
69+
t.Fatal(err)
70+
}
71+
72+
methodID := fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod)
73+
authorizerID := fmt.Sprintf("%s/%s", *input.RestApiId, "authorizer-id")
74+
validatorID := fmt.Sprintf("%s/%s", *input.RestApiId, "request-validator-id")
75+
76+
tests := adapterhelpers.QueryTests{
77+
{
78+
ExpectedType: "apigateway-integration",
79+
ExpectedMethod: sdp.QueryMethod_GET,
80+
ExpectedQuery: methodID,
81+
ExpectedScope: "scope",
82+
},
83+
{
84+
ExpectedType: "apigateway-authorizer",
85+
ExpectedMethod: sdp.QueryMethod_GET,
86+
ExpectedQuery: authorizerID,
87+
ExpectedScope: "scope",
88+
},
89+
{
90+
ExpectedType: "apigateway-request-validator",
91+
ExpectedMethod: sdp.QueryMethod_GET,
92+
ExpectedQuery: validatorID,
93+
ExpectedScope: "scope",
94+
},
95+
}
96+
97+
tests.Execute(t, item)
98+
}
99+
100+
func TestNewAPIGatewayMethodAdapter(t *testing.T) {
101+
config, account, region := adapterhelpers.GetAutoConfig(t)
102+
103+
client := apigateway.NewFromConfig(config)
104+
105+
adapter := NewAPIGatewayMethodAdapter(client, account, region)
106+
107+
test := adapterhelpers.E2ETest{
108+
Adapter: adapter,
109+
Timeout: 10 * time.Second,
110+
SkipList: true,
111+
}
112+
113+
test.Run(t)
114+
}

adapters/integration/apigateway/apigateway_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package apigateway
22

33
import (
44
"context"
5+
"fmt"
56
"testing"
67

78
"github.com/overmindtech/aws-source/adapterhelpers"
@@ -42,6 +43,13 @@ func APIGateway(t *testing.T) {
4243
t.Fatalf("failed to validate APIGateway resource adapter: %v", err)
4344
}
4445

46+
methodSource := adapters.NewAPIGatewayMethodAdapter(testClient, accountID, testAWSConfig.Region)
47+
48+
err = methodSource.Validate()
49+
if err != nil {
50+
t.Fatalf("failed to validate APIGateway method adapter: %v", err)
51+
}
52+
4553
scope := adapterhelpers.FormatScope(accountID, testAWSConfig.Region)
4654

4755
// List restApis
@@ -155,4 +163,20 @@ func APIGateway(t *testing.T) {
155163
if resourceUniqueAttrFromSearch != resourceUniqueAttrFromGet {
156164
t.Fatalf("expected resource ID %s, got %s", resourceUniqueAttrFromSearch, resourceUniqueAttrFromGet)
157165
}
166+
167+
// Get method
168+
methodID := fmt.Sprintf("%s/GET", resourceUniqueAttrFromGet) // resourceUniqueAttribute contains the restApiID
169+
method, err := methodSource.Get(ctx, scope, methodID, true)
170+
if err != nil {
171+
t.Fatalf("failed to get APIGateway method: %v", err)
172+
}
173+
174+
uniqueMethodAttr, err := method.GetAttributes().Get(method.GetUniqueAttribute())
175+
if err != nil {
176+
t.Fatalf("failed to get unique method attribute: %v", err)
177+
}
178+
179+
if uniqueMethodAttr != methodID {
180+
t.Fatalf("expected method ID %s, got %s", methodID, uniqueMethodAttr)
181+
}
158182
}

adapters/integration/apigateway/create.go

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"errors"
66
"log/slog"
7+
"strings"
78

89
"github.com/aws/aws-sdk-go-v2/service/apigateway"
910
"github.com/overmindtech/aws-source/adapterhelpers"
@@ -57,11 +58,49 @@ func createResource(ctx context.Context, logger *slog.Logger, client *apigateway
5758
result, err := client.CreateResource(ctx, &apigateway.CreateResourceInput{
5859
RestApiId: restAPIID,
5960
ParentId: parentID,
60-
PathPart: adapterhelpers.PtrString(path),
61+
PathPart: adapterhelpers.PtrString(cleanPath(path)),
6162
})
6263
if err != nil {
6364
return nil, err
6465
}
6566

6667
return result.Id, nil
6768
}
69+
70+
func cleanPath(path string) string {
71+
p, ok := strings.CutPrefix(path, "/")
72+
if !ok {
73+
return path
74+
}
75+
76+
return p
77+
}
78+
79+
func createMethod(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID, resourceID *string, method string) error {
80+
// check if a method with the same name already exists
81+
err := findMethod(ctx, client, restAPIID, resourceID, method)
82+
if err != nil {
83+
if errors.As(err, new(integration.NotFoundError)) {
84+
logger.InfoContext(ctx, "Creating method")
85+
} else {
86+
return err
87+
}
88+
}
89+
90+
if err == nil {
91+
logger.InfoContext(ctx, "Method already exists")
92+
return nil
93+
}
94+
95+
_, err = client.PutMethod(ctx, &apigateway.PutMethodInput{
96+
RestApiId: restAPIID,
97+
ResourceId: resourceID,
98+
HttpMethod: adapterhelpers.PtrString(method),
99+
AuthorizationType: adapterhelpers.PtrString("NONE"),
100+
})
101+
if err != nil {
102+
return err
103+
}
104+
105+
return nil
106+
}

0 commit comments

Comments
 (0)