Skip to content

Commit

Permalink
add apigateway method response adapter
Browse files Browse the repository at this point in the history
  • Loading branch information
tomasdembelli committed Jan 5, 2025
1 parent 00a3057 commit 6e9d1d7
Show file tree
Hide file tree
Showing 8 changed files with 275 additions and 3 deletions.
106 changes: 106 additions & 0 deletions adapters/apigateway-method-response.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package adapters

import (
"context"
"fmt"
"log/slog"
"strings"

"github.com/aws/aws-sdk-go-v2/service/apigateway"
"github.com/overmindtech/aws-source/adapterhelpers"
"github.com/overmindtech/sdp-go"
)

func apiGatewayMethodResponseGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodResponseInput) (*sdp.Item, error) {
output, err := client.GetMethodResponse(ctx, input)
if err != nil {
return nil, err
}

attributes, err := adapterhelpers.ToAttributesWithExclude(output, "tags")
if err != nil {
return nil, err
}

// We create a custom ID of {rest-api-id}/{resource-id}/{http-method}/{status-code} e.g.
// rest-api-id/resource-id/GET/200
methodResponseID := fmt.Sprintf(
"%s/%s/%s/%s",
*input.RestApiId,
*input.ResourceId,
*input.HttpMethod,
*input.StatusCode,
)
err = attributes.Set("MethodResponseID", methodResponseID)
if err != nil {
return nil, err
}

item := &sdp.Item{
Type: "apigateway-method-response",
UniqueAttribute: "MethodResponseID",
Attributes: attributes,
Scope: scope,
}

item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{
Query: &sdp.Query{
Type: "apigateway-method",
Method: sdp.QueryMethod_GET,
Query: fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod),
Scope: scope,
},
BlastPropagation: &sdp.BlastPropagation{
// They are tightly coupled
In: true,
Out: true,
},
})

return item, nil
}

func NewAPIGatewayMethodResponseAdapter(client apigatewayClient, accountID string, region string) *adapterhelpers.AlwaysGetAdapter[*apigateway.GetMethodResponseInput, *apigateway.GetMethodResponseOutput, *apigateway.GetMethodResponseInput, *apigateway.GetMethodResponseOutput, apigatewayClient, *apigateway.Options] {
return &adapterhelpers.AlwaysGetAdapter[*apigateway.GetMethodResponseInput, *apigateway.GetMethodResponseOutput, *apigateway.GetMethodResponseInput, *apigateway.GetMethodResponseOutput, apigatewayClient, *apigateway.Options]{
ItemType: "apigateway-method-response",
Client: client,
AccountID: accountID,
Region: region,
AdapterMetadata: apiGatewayMethodResponseAdapterMetadata,
GetFunc: apiGatewayMethodResponseGetFunc,
GetInputMapper: func(scope, query string) *apigateway.GetMethodResponseInput {
// We are using a custom id of {rest-api-id}/{resource-id}/{http-method}/{status-code} e.g.
// rest-api-id/resource-id/GET/200
f := strings.Split(query, "/")
if len(f) != 4 {
slog.Error(
"query must be in the format of: rest-api-id/resource-id/http-method/status-code",
"found",
query,
)

return nil
}

return &apigateway.GetMethodResponseInput{
RestApiId: &f[0],
ResourceId: &f[1],
HttpMethod: &f[2],
StatusCode: &f[3],
}
},
DisableList: true,
}
}

var apiGatewayMethodResponseAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{
Type: "apigateway-method-response",
DescriptiveName: "API Gateway Method Response",
Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK,
SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{
Get: true,
GetDescription: "Get a Method Response by rest-api id, resource id, http-method, and status-code",
Search: true,
SearchDescription: "Search Method Responses by ARN",
},
})
72 changes: 72 additions & 0 deletions adapters/apigateway-method-response_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package adapters

import (
"context"
"fmt"
"testing"
"time"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/apigateway"
"github.com/overmindtech/aws-source/adapterhelpers"
"github.com/overmindtech/sdp-go"
)

func (m *mockAPIGatewayClient) GetMethodResponse(ctx context.Context, params *apigateway.GetMethodResponseInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodResponseOutput, error) {
return &apigateway.GetMethodResponseOutput{
ResponseModels: map[string]string{
"application/json": "Empty",
},
StatusCode: aws.String("200"),
}, nil
}

func TestApiGatewayMethodResponseGetFunc(t *testing.T) {
ctx := context.Background()
cli := mockAPIGatewayClient{}

input := &apigateway.GetMethodResponseInput{
RestApiId: aws.String("rest-api-id"),
ResourceId: aws.String("resource-id"),
HttpMethod: aws.String("GET"),
StatusCode: aws.String("200"),
}

item, err := apiGatewayMethodResponseGetFunc(ctx, &cli, "scope", input)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}

if err = item.Validate(); err != nil {
t.Fatal(err)
}

methodID := fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod)

tests := adapterhelpers.QueryTests{
{
ExpectedType: "apigateway-method",
ExpectedMethod: sdp.QueryMethod_GET,
ExpectedQuery: methodID,
ExpectedScope: "scope",
},
}

tests.Execute(t, item)
}

func TestNewAPIGatewayMethodResponseAdapter(t *testing.T) {
config, account, region := adapterhelpers.GetAutoConfig(t)

client := apigateway.NewFromConfig(config)

adapter := NewAPIGatewayMethodResponseAdapter(client, account, region)

test := adapterhelpers.E2ETest{
Adapter: adapter,
Timeout: 10 * time.Second,
SkipList: true,
}

test.Run(t)
}
1 change: 1 addition & 0 deletions adapters/apigateway-method.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

type apigatewayClient interface {
GetMethod(ctx context.Context, params *apigateway.GetMethodInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error)
GetMethodResponse(ctx context.Context, params *apigateway.GetMethodResponseInput, optFns ...func(*apigateway.Options)) (*apigateway.GetMethodResponseOutput, error)
}

func apiGatewayMethodGetFunc(ctx context.Context, client apigatewayClient, scope string, input *apigateway.GetMethodInput) (*sdp.Item, error) {
Expand Down
25 changes: 25 additions & 0 deletions adapters/integration/apigateway/apigateway_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,13 @@ func APIGateway(t *testing.T) {
t.Fatalf("failed to validate APIGateway method adapter: %v", err)
}

methodResponseSource := adapters.NewAPIGatewayMethodResponseAdapter(testClient, accountID, testAWSConfig.Region)

err = methodResponseSource.Validate()
if err != nil {
t.Fatalf("failed to validate APIGateway method response adapter: %v", err)
}

scope := adapterhelpers.FormatScope(accountID, testAWSConfig.Region)

// List restApis
Expand Down Expand Up @@ -179,4 +186,22 @@ func APIGateway(t *testing.T) {
if uniqueMethodAttr != methodID {
t.Fatalf("expected method ID %s, got %s", methodID, uniqueMethodAttr)
}

// Get method response
methodResponseID := fmt.Sprintf("%s/200", methodID)
methodResponse, err := methodResponseSource.Get(ctx, scope, methodResponseID, true)
if err != nil {
t.Fatalf("failed to get APIGateway method response: %v", err)
}

uniqueMethodResponseAttr, err := methodResponse.GetAttributes().Get(methodResponse.GetUniqueAttribute())
if err != nil {
t.Fatalf("failed to get unique method response attribute: %v", err)
}

if uniqueMethodResponseAttr != methodResponseID {
t.Fatalf("expected method response ID %s, got %s", methodResponseID, uniqueMethodResponseAttr)
}

t.Log("APIGateway integration test completed")
}
35 changes: 35 additions & 0 deletions adapters/integration/apigateway/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,38 @@ func createMethod(ctx context.Context, logger *slog.Logger, client *apigateway.C

return nil
}

func createMethodResponse(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID, resourceID *string, method, statusCode string) error {
// check if a method response with the same status code already exists
err := findMethodResponse(ctx, client, restAPIID, resourceID, method, statusCode)
if err != nil {
if errors.As(err, new(integration.NotFoundError)) {
logger.InfoContext(ctx, "Creating method response")
} else {
return err
}
}

if err == nil {
logger.InfoContext(ctx, "Method response already exists")
return nil
}

_, err = client.PutMethodResponse(ctx, &apigateway.PutMethodResponseInput{
RestApiId: restAPIID,
ResourceId: resourceID,
HttpMethod: adapterhelpers.PtrString(method),
StatusCode: adapterhelpers.PtrString(statusCode),
ResponseModels: map[string]string{
"application/json": "Empty",
},
ResponseParameters: map[string]bool{
"method.response.header.Content-Type": true,
},
})
if err != nil {
return err
}

return nil
}
25 changes: 25 additions & 0 deletions adapters/integration/apigateway/find.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,28 @@ func findMethod(ctx context.Context, client *apigateway.Client, restAPIID, resou

return nil
}

func findMethodResponse(ctx context.Context, client *apigateway.Client, restAPIID, resourceID *string, method string, statusCode string) error {
_, err := client.GetMethodResponse(ctx, &apigateway.GetMethodResponseInput{
RestApiId: restAPIID,
ResourceId: resourceID,
HttpMethod: &method,
StatusCode: &statusCode,
})

if err != nil {
var notFoundErr *types.NotFoundException
if errors.As(err, &notFoundErr) {
return integration.NewNotFoundError(integration.ResourceName(
integration.APIGateway,
methodResponseSrc,
method,
statusCode,
))
}

return err
}

return nil
}
13 changes: 10 additions & 3 deletions adapters/integration/apigateway/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@ import (
)

const (
restAPISrc = "rest-api"
resourceSrc = "resource"
methodSrc = "method"
restAPISrc = "rest-api"
resourceSrc = "resource"
methodSrc = "method"
methodResponseSrc = "method-response"
)

func setup(ctx context.Context, logger *slog.Logger, client *apigateway.Client) error {
Expand Down Expand Up @@ -41,5 +42,11 @@ func setup(ctx context.Context, logger *slog.Logger, client *apigateway.Client)
return err
}

// Create method response
err = createMethodResponse(ctx, logger, client, restApiID, testResourceID, "GET", "200")
if err != nil {
return err
}

return nil
}
1 change: 1 addition & 0 deletions proc/proc.go
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,7 @@ func InitializeAwsSourceEngine(ctx context.Context, ec *discovery.EngineConfig,
adapters.NewAPIGatewayResourceAdapter(apigatewayClient, *callerID.Account, cfg.Region),
adapters.NewAPIGatewayDomainNameAdapter(apigatewayClient, *callerID.Account, cfg.Region),
adapters.NewAPIGatewayMethodAdapter(apigatewayClient, *callerID.Account, cfg.Region),
adapters.NewAPIGatewayMethodResponseAdapter(apigatewayClient, *callerID.Account, cfg.Region),

// SSM
adapters.NewSSMParameterAdapter(ssmClient, *callerID.Account, cfg.Region),
Expand Down

0 comments on commit 6e9d1d7

Please sign in to comment.