diff --git a/adapters/apigateway-method-response.go b/adapters/apigateway-method-response.go new file mode 100644 index 00000000..90385cf2 --- /dev/null +++ b/adapters/apigateway-method-response.go @@ -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", + }, +}) diff --git a/adapters/apigateway-method-response_test.go b/adapters/apigateway-method-response_test.go new file mode 100644 index 00000000..e7228738 --- /dev/null +++ b/adapters/apigateway-method-response_test.go @@ -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) +} diff --git a/adapters/apigateway-method.go b/adapters/apigateway-method.go index ce9022a3..0cba12d8 100644 --- a/adapters/apigateway-method.go +++ b/adapters/apigateway-method.go @@ -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) { diff --git a/adapters/integration/apigateway/apigateway_test.go b/adapters/integration/apigateway/apigateway_test.go index 442c85b1..f547848b 100644 --- a/adapters/integration/apigateway/apigateway_test.go +++ b/adapters/integration/apigateway/apigateway_test.go @@ -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 @@ -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") } diff --git a/adapters/integration/apigateway/create.go b/adapters/integration/apigateway/create.go index 976a959b..94517051 100644 --- a/adapters/integration/apigateway/create.go +++ b/adapters/integration/apigateway/create.go @@ -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 +} diff --git a/adapters/integration/apigateway/find.go b/adapters/integration/apigateway/find.go index 42b52abe..cf146509 100644 --- a/adapters/integration/apigateway/find.go +++ b/adapters/integration/apigateway/find.go @@ -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, ¬FoundErr) { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + methodResponseSrc, + method, + statusCode, + )) + } + + return err + } + + return nil +} diff --git a/adapters/integration/apigateway/setup.go b/adapters/integration/apigateway/setup.go index c8285643..46f29b6f 100644 --- a/adapters/integration/apigateway/setup.go +++ b/adapters/integration/apigateway/setup.go @@ -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 { @@ -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 } diff --git a/proc/proc.go b/proc/proc.go index 98642c2a..b1b1dec1 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -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),