diff --git a/adapters/apigateway-api-key.go b/adapters/apigateway-api-key.go new file mode 100644 index 00000000..53eeabf5 --- /dev/null +++ b/adapters/apigateway-api-key.go @@ -0,0 +1,129 @@ +package adapters + +import ( + "context" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" + "strings" +) + +// convertGetApiKeyOutputToApiKey converts a GetApiKeyOutput to an ApiKey +func convertGetApiKeyOutputToApiKey(output *apigateway.GetApiKeyOutput) *types.ApiKey { + return &types.ApiKey{ + Id: output.Id, + Name: output.Name, + Enabled: output.Enabled, + CreatedDate: output.CreatedDate, + LastUpdatedDate: output.LastUpdatedDate, + StageKeys: output.StageKeys, + Tags: output.Tags, + } +} + +func apiKeyListFunc(ctx context.Context, client *apigateway.Client, _ string) ([]*types.ApiKey, error) { + out, err := client.GetApiKeys(ctx, &apigateway.GetApiKeysInput{}) + if err != nil { + return nil, err + } + + var items []*types.ApiKey + for _, apiKey := range out.Items { + items = append(items, &apiKey) + } + + return items, nil +} + +func apiKeyOutputMapper(scope string, awsItem *types.ApiKey) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + item := sdp.Item{ + Type: "apigateway-api-key", + UniqueAttribute: "Id", + Attributes: attributes, + Scope: scope, + Tags: awsItem.Tags, + } + + for _, key := range awsItem.StageKeys { + // {restApiId}/{stage} + restAPIID := strings.Split(key, "/")[0] + if restAPIID != "" { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: restAPIID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate both ways + In: true, + Out: true, + }, + }) + } + } + + return &item, nil +} + +func NewAPIGatewayApiKeyAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.ApiKey, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.ApiKey, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-api-key", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: apiKeyAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.ApiKey, error) { + out, err := client.GetApiKey(ctx, &apigateway.GetApiKeyInput{ + ApiKey: &query, + }) + if err != nil { + return nil, err + } + return convertGetApiKeyOutputToApiKey(out), nil + }, + ListFunc: apiKeyListFunc, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.ApiKey, error) { + out, err := client.GetApiKeys(ctx, &apigateway.GetApiKeysInput{ + NameQuery: &query, + }) + if err != nil { + return nil, err + } + + var items []*types.ApiKey + for _, apiKey := range out.Items { + items = append(items, &apiKey) + } + + return items, nil + }, + ItemMapper: func(_, scope string, awsItem *types.ApiKey) (*sdp.Item, error) { + return apiKeyOutputMapper(scope, awsItem) + }, + } +} + +var apiKeyAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-api-key", + DescriptiveName: "API Key", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get an API Key by ID", + ListDescription: "List all API Keys", + SearchDescription: "Search for API Keys by their name", + }, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_api_key.id"}, + }, +}) diff --git a/adapters/apigateway-api-key_test.go b/adapters/apigateway-api-key_test.go new file mode 100644 index 00000000..3380323b --- /dev/null +++ b/adapters/apigateway-api-key_test.go @@ -0,0 +1,60 @@ +package adapters + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +func TestApiKeyOutputMapper(t *testing.T) { + awsItem := &types.ApiKey{ + Id: aws.String("api-key-id"), + Name: aws.String("api-key-name"), + Enabled: true, + CreatedDate: aws.Time(time.Now()), + LastUpdatedDate: aws.Time(time.Now()), + StageKeys: []string{"rest-api-id/stage"}, + Tags: map[string]string{"key": "value"}, + } + + item, err := apiKeyOutputMapper("scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayApiKeyAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayApiKeyAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-authorizer.go b/adapters/apigateway-authorizer.go new file mode 100644 index 00000000..c418c1e3 --- /dev/null +++ b/adapters/apigateway-authorizer.go @@ -0,0 +1,143 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +// convertGetAuthorizerOutputToAuthorizer converts a GetAuthorizerOutput to an Authorizer +func convertGetAuthorizerOutputToAuthorizer(output *apigateway.GetAuthorizerOutput) *types.Authorizer { + return &types.Authorizer{ + Id: output.Id, + Name: output.Name, + Type: output.Type, + ProviderARNs: output.ProviderARNs, + AuthType: output.AuthType, + AuthorizerUri: output.AuthorizerUri, + AuthorizerCredentials: output.AuthorizerCredentials, + IdentitySource: output.IdentitySource, + IdentityValidationExpression: output.IdentityValidationExpression, + AuthorizerResultTtlInSeconds: output.AuthorizerResultTtlInSeconds, + } +} + +func authorizerOutputMapper(query, scope string, awsItem *types.Authorizer) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + item := sdp.Item{ + Type: "apigateway-authorizer", + UniqueAttribute: "Id", + Attributes: attributes, + Scope: scope, + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: strings.Split(query, "/")[0], + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + + return &item, nil +} + +func NewAPIGatewayAuthorizerAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.Authorizer, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.Authorizer, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-authorizer", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: authorizerAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.Authorizer, error) { + f := strings.Split(query, "/") + if len(f) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("query must be in the format of: the rest-api-id/authorizer-id, but found: %s", query), + } + } + out, err := client.GetAuthorizer(ctx, &apigateway.GetAuthorizerInput{ + RestApiId: &f[0], + AuthorizerId: &f[1], + }) + if err != nil { + return nil, err + } + return convertGetAuthorizerOutputToAuthorizer(out), nil + }, + DisableList: true, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.Authorizer, error) { + f := strings.Split(query, "/") + var restAPIID string + var name string + + switch len(f) { + case 1: + restAPIID = f[0] + case 2: + restAPIID = f[0] + name = f[1] + default: + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf( + "query must be in the format of: the rest-api-id/authorizer-id or rest-api-id, but found: %s", + query, + ), + } + } + + out, err := client.GetAuthorizers(ctx, &apigateway.GetAuthorizersInput{ + RestApiId: &restAPIID, + }) + if err != nil { + return nil, err + } + + var items []*types.Authorizer + for _, authorizer := range out.Items { + if name != "" && strings.Contains(*authorizer.Name, name) { + items = append(items, &authorizer) + } else { + items = append(items, &authorizer) + } + } + + return items, nil + }, + ItemMapper: func(query, scope string, awsItem *types.Authorizer) (*sdp.Item, error) { + return authorizerOutputMapper(query, scope, awsItem) + }, + } +} + +var authorizerAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-authorizer", + DescriptiveName: "API Gateway Authorizer", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_SECURITY, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + Search: true, + GetDescription: "Get an API Gateway Authorizer by its rest API ID and ID: rest-api-id/authorizer-id", + SearchDescription: "Search for API Gateway Authorizers by their rest API ID or with rest API ID and their name: rest-api-id/authorizer-name", + }, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_authorizer.id"}, + }, +}) diff --git a/adapters/apigateway-authorizer_test.go b/adapters/apigateway-authorizer_test.go new file mode 100644 index 00000000..579d7202 --- /dev/null +++ b/adapters/apigateway-authorizer_test.go @@ -0,0 +1,63 @@ +package adapters + +import ( + "github.com/overmindtech/sdp-go" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" +) + +func TestAuthorizerOutputMapper(t *testing.T) { + awsItem := &types.Authorizer{ + Id: aws.String("authorizer-id"), + Name: aws.String("authorizer-name"), + Type: types.AuthorizerTypeRequest, + ProviderARNs: []string{"arn:aws:iam::123456789012:role/service-role"}, + AuthType: aws.String("custom"), + AuthorizerUri: aws.String("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:my-function/invocations"), + AuthorizerCredentials: aws.String("arn:aws:iam::123456789012:role/service-role"), + IdentitySource: aws.String("method.request.header.Authorization"), + IdentityValidationExpression: aws.String(".*"), + AuthorizerResultTtlInSeconds: aws.Int32(300), + } + + item, err := authorizerOutputMapper("rest-api-id", "scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayAuthorizerAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayAuthorizerAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-base-path-mapping.go b/adapters/apigateway-base-path-mapping.go new file mode 100644 index 00000000..b73eb319 --- /dev/null +++ b/adapters/apigateway-base-path-mapping.go @@ -0,0 +1,129 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +func convertGetBasePathMappingOutputToBasePathMapping(output *apigateway.GetBasePathMappingOutput) *types.BasePathMapping { + return &types.BasePathMapping{ + BasePath: output.BasePath, + RestApiId: output.RestApiId, + Stage: output.Stage, + } +} + +func basePathMappingOutputMapper(query, scope string, awsItem *types.BasePathMapping) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + domainName := strings.Split(query, "/")[0] + + err = attributes.Set("UniqueAttribute", fmt.Sprintf("%s/%s", domainName, *awsItem.BasePath)) + + item := sdp.Item{ + Type: "apigateway-base-path-mapping", + UniqueAttribute: "BasePath", + Attributes: attributes, + Scope: scope, + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-domain-name", + Method: sdp.QueryMethod_GET, + Query: domainName, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // If domain name changes, the base path mapping will be affected + In: true, + // If base path mapping changes, the domain name won't be affected + Out: false, + }, + }) + + if awsItem.RestApiId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: *awsItem.RestApiId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + } + + return &item, nil +} + +func NewAPIGatewayBasePathMappingAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.BasePathMapping, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.BasePathMapping, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-base-path-mapping", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: basePathMappingAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.BasePathMapping, error) { + f := strings.Split(query, "/") + if len(f) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("query must be in the format of: the domain-name/base-path, but found: %s", query), + } + } + out, err := client.GetBasePathMapping(ctx, &apigateway.GetBasePathMappingInput{ + DomainName: &f[0], + BasePath: &f[1], + }) + if err != nil { + return nil, err + } + return convertGetBasePathMappingOutputToBasePathMapping(out), nil + }, + DisableList: true, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.BasePathMapping, error) { + out, err := client.GetBasePathMappings(ctx, &apigateway.GetBasePathMappingsInput{ + DomainName: &query, + }) + if err != nil { + return nil, err + } + + var items []*types.BasePathMapping + for _, basePathMapping := range out.Items { + items = append(items, &basePathMapping) + } + + return items, nil + }, + ItemMapper: func(query, scope string, awsItem *types.BasePathMapping) (*sdp.Item, error) { + return basePathMappingOutputMapper(query, scope, awsItem) + }, + } +} + +var basePathMappingAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-base-path-mapping", + DescriptiveName: "API Gateway Base Path Mapping", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + Search: true, + GetDescription: "Get an API Gateway Base Path Mapping by its domain name and base path: domain-name/base-path", + SearchDescription: "Search for API Gateway Base Path Mappings by their domain name: domain-name", + }, +}) diff --git a/adapters/apigateway-base-path-mapping_test.go b/adapters/apigateway-base-path-mapping_test.go new file mode 100644 index 00000000..d188bf07 --- /dev/null +++ b/adapters/apigateway-base-path-mapping_test.go @@ -0,0 +1,62 @@ +package adapters + +import ( + "github.com/overmindtech/sdp-go" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" +) + +func TestBasePathMappingOutputMapper(t *testing.T) { + awsItem := &types.BasePathMapping{ + BasePath: aws.String("base-path"), + RestApiId: aws.String("rest-api-id"), + Stage: aws.String("stage"), + } + + item, err := basePathMappingOutputMapper("domain-name", "scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-domain-name", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "domain-name", + ExpectedScope: "scope", + }, + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayBasePathMappingAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayBasePathMappingAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-deployment.go b/adapters/apigateway-deployment.go new file mode 100644 index 00000000..2b8e5008 --- /dev/null +++ b/adapters/apigateway-deployment.go @@ -0,0 +1,137 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +// convertGetDeploymentOutputToDeployment converts a GetDeploymentOutput to a Deployment +func convertGetDeploymentOutputToDeployment(output *apigateway.GetDeploymentOutput) *types.Deployment { + return &types.Deployment{ + Id: output.Id, + CreatedDate: output.CreatedDate, + Description: output.Description, + ApiSummary: output.ApiSummary, + } +} + +func deploymentOutputMapper(query, scope string, awsItem *types.Deployment) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + item := sdp.Item{ + Type: "apigateway-deployment", + UniqueAttribute: "Id", + Attributes: attributes, + Scope: scope, + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: strings.Split(query, "/")[0], + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + + return &item, nil +} + +func NewAPIGatewayDeploymentAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.Deployment, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.Deployment, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-deployment", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: deploymentAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.Deployment, error) { + f := strings.Split(query, "/") + if len(f) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("query must be in the format of: the rest-api-id/deployment-id, but found: %s", query), + } + } + out, err := client.GetDeployment(ctx, &apigateway.GetDeploymentInput{ + RestApiId: &f[0], + DeploymentId: &f[1], + }) + if err != nil { + return nil, err + } + return convertGetDeploymentOutputToDeployment(out), nil + }, + DisableList: true, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.Deployment, error) { + f := strings.Split(query, "/") + var restAPIID string + var description string + + switch len(f) { + case 1: + restAPIID = f[0] + case 2: + restAPIID = f[0] + description = f[1] + default: + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf( + "query must be in the format of: the rest-api-id/deployment-id or rest-api-id, but found: %s", + query, + ), + } + } + + out, err := client.GetDeployments(ctx, &apigateway.GetDeploymentsInput{ + RestApiId: &restAPIID, + }) + if err != nil { + return nil, err + } + + var items []*types.Deployment + for _, deployment := range out.Items { + if description != "" && strings.Contains(*deployment.Description, description) { + items = append(items, &deployment) + } else { + items = append(items, &deployment) + } + } + + return items, nil + }, + ItemMapper: func(query, scope string, awsItem *types.Deployment) (*sdp.Item, error) { + return deploymentOutputMapper(query, scope, awsItem) + }, + } +} + +var deploymentAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-deployment", + DescriptiveName: "API Gateway Deployment", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + Search: true, + GetDescription: "Get an API Gateway Deployment by its rest API ID and ID: rest-api-id/deployment-id", + SearchDescription: "Search for API Gateway Deployments by their rest API ID or with rest API ID and their description: rest-api-id/deployment-description", + }, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_deployment.id"}, + }, +}) diff --git a/adapters/apigateway-deployment_test.go b/adapters/apigateway-deployment_test.go new file mode 100644 index 00000000..49a585ed --- /dev/null +++ b/adapters/apigateway-deployment_test.go @@ -0,0 +1,57 @@ +package adapters + +import ( + "github.com/overmindtech/sdp-go" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" +) + +func TestDeploymentOutputMapper(t *testing.T) { + awsItem := &types.Deployment{ + Id: aws.String("deployment-id"), + CreatedDate: aws.Time(time.Now()), + Description: aws.String("deployment-description"), + ApiSummary: map[string]map[string]types.MethodSnapshot{}, + } + + item, err := deploymentOutputMapper("rest-api-id", "scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayDeploymentAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayDeploymentAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-domain-name.go b/adapters/apigateway-domain-name.go index 07ee88e2..b5dc2e85 100644 --- a/adapters/apigateway-domain-name.go +++ b/adapters/apigateway-domain-name.go @@ -235,6 +235,6 @@ var apiGatewayDomainNameAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata }, PotentialLinks: []string{"acm-certificate"}, TerraformMappings: []*sdp.TerraformMapping{ - {TerraformQueryMap: "aws_api_gateway_domain_name.domain_name"}, + {TerraformQueryMap: "aws_api_gateway_domain_name.id"}, }, }) diff --git a/adapters/apigateway-integration.go b/adapters/apigateway-integration.go new file mode 100644 index 00000000..2b72e8c8 --- /dev/null +++ b/adapters/apigateway-integration.go @@ -0,0 +1,125 @@ +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" +) + +type apiGatewayIntegrationGetter interface { + GetIntegration(ctx context.Context, params *apigateway.GetIntegrationInput, optFns ...func(*apigateway.Options)) (*apigateway.GetIntegrationOutput, error) +} + +func apiGatewayIntegrationGetFunc(ctx context.Context, client apiGatewayIntegrationGetter, scope string, input *apigateway.GetIntegrationInput) (*sdp.Item, error) { + output, err := client.GetIntegration(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} e.g. + // rest-api-id/resource-id/GET + integrationID := fmt.Sprintf( + "%s/%s/%s", + *input.RestApiId, + *input.ResourceId, + *input.HttpMethod, + ) + err = attributes.Set("IntegrationID", integrationID) + if err != nil { + return nil, err + } + + item := &sdp.Item{ + Type: "apigateway-integration", + UniqueAttribute: "IntegrationID", + 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, + }, + }) + + if output.ConnectionId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-vpc-link", + Method: sdp.QueryMethod_GET, + Query: *output.ConnectionId, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // If VPC link goes away, so does the integration + In: true, + // If integration goes away, VPC link is still there + Out: false, + }, + }) + } + + return item, nil +} + +func NewAPIGatewayIntegrationAdapter(client apiGatewayIntegrationGetter, accountID string, region string) *adapterhelpers.AlwaysGetAdapter[*apigateway.GetIntegrationInput, *apigateway.GetIntegrationOutput, *apigateway.GetIntegrationInput, *apigateway.GetIntegrationOutput, apiGatewayIntegrationGetter, *apigateway.Options] { + return &adapterhelpers.AlwaysGetAdapter[*apigateway.GetIntegrationInput, *apigateway.GetIntegrationOutput, *apigateway.GetIntegrationInput, *apigateway.GetIntegrationOutput, apiGatewayIntegrationGetter, *apigateway.Options]{ + ItemType: "apigateway-integration", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: apiGatewayIntegrationAdapterMetadata, + GetFunc: apiGatewayIntegrationGetFunc, + GetInputMapper: func(scope, query string) *apigateway.GetIntegrationInput { + // We are using a custom id of {rest-api-id}/{resource-id}/{http-method} e.g. + // rest-api-id/resource-id/GET + f := strings.Split(query, "/") + if len(f) != 3 { + slog.Error( + "query must be in the format of: rest-api-id/resource-id/http-method", + "found", + query, + ) + + return nil + } + + return &apigateway.GetIntegrationInput{ + RestApiId: &f[0], + ResourceId: &f[1], + HttpMethod: &f[2], + } + }, + DisableList: true, + } +} + +var apiGatewayIntegrationAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-integration", + DescriptiveName: "API Gateway Integration", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + GetDescription: "Get an Integration by rest-api id, resource id, and http-method", + Search: true, + SearchDescription: "Search Integrations by ARN", + }, +}) diff --git a/adapters/apigateway-integration_test.go b/adapters/apigateway-integration_test.go new file mode 100644 index 00000000..2d012182 --- /dev/null +++ b/adapters/apigateway-integration_test.go @@ -0,0 +1,90 @@ +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/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +type mockAPIGatewayIntegrationClient struct{} + +func (m *mockAPIGatewayIntegrationClient) GetIntegration(ctx context.Context, params *apigateway.GetIntegrationInput, optFns ...func(*apigateway.Options)) (*apigateway.GetIntegrationOutput, error) { + return &apigateway.GetIntegrationOutput{ + IntegrationResponses: map[string]types.IntegrationResponse{ + "200": { + ResponseTemplates: map[string]string{ + "application/json": "", + }, + StatusCode: aws.String("200"), + }, + }, + CacheKeyParameters: []string{}, + 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"), + HttpMethod: aws.String("POST"), + CacheNamespace: aws.String("y9h6rt"), + Type: "AWS", + ConnectionId: aws.String("vpc-connection-id"), + }, nil +} + +func TestApiGatewayIntegrationGetFunc(t *testing.T) { + ctx := context.Background() + cli := mockAPIGatewayIntegrationClient{} + + input := &apigateway.GetIntegrationInput{ + RestApiId: aws.String("rest-api-id"), + ResourceId: aws.String("resource-id"), + HttpMethod: aws.String("GET"), + } + + item, err := apiGatewayIntegrationGetFunc(ctx, &cli, "scope", input) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err = item.Validate(); err != nil { + t.Fatal(err) + } + + integrationID := fmt.Sprintf("%s/%s/%s", *input.RestApiId, *input.ResourceId, *input.HttpMethod) + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-method", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: integrationID, + ExpectedScope: "scope", + }, + { + ExpectedType: "apigateway-vpc-link", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "vpc-connection-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayIntegrationAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayIntegrationAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-model.go b/adapters/apigateway-model.go new file mode 100644 index 00000000..a2c83d18 --- /dev/null +++ b/adapters/apigateway-model.go @@ -0,0 +1,117 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +func convertGetModelOutputToModel(output *apigateway.GetModelOutput) *types.Model { + return &types.Model{ + Id: output.Id, + Name: output.Name, + Description: output.Description, + Schema: output.Schema, + ContentType: output.ContentType, + } +} + +func modelOutputMapper(query, scope string, awsItem *types.Model) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + restAPIID := strings.Split(query, "/")[0] + + err = attributes.Set("UniqueAttribute", fmt.Sprintf("%s/%s", restAPIID, *awsItem.Name)) + + item := sdp.Item{ + Type: "apigateway-model", + UniqueAttribute: "Name", + Attributes: attributes, + Scope: scope, + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: restAPIID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + + return &item, nil +} + +func NewAPIGatewayModelAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.Model, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.Model, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-model", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: modelAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.Model, error) { + f := strings.Split(query, "/") + if len(f) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("query must be in the format of: the rest-api-id/model-name, but found: %s", query), + } + } + out, err := client.GetModel(ctx, &apigateway.GetModelInput{ + RestApiId: &f[0], + ModelName: &f[1], + }) + if err != nil { + return nil, err + } + return convertGetModelOutputToModel(out), nil + }, + DisableList: true, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.Model, error) { + out, err := client.GetModels(ctx, &apigateway.GetModelsInput{ + RestApiId: &query, + }) + if err != nil { + return nil, err + } + + var items []*types.Model + for _, model := range out.Items { + items = append(items, &model) + } + + return items, nil + }, + ItemMapper: func(query, scope string, awsItem *types.Model) (*sdp.Item, error) { + return modelOutputMapper(query, scope, awsItem) + }, + } +} + +var modelAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-model", + DescriptiveName: "API Gateway Model", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + Search: true, + GetDescription: "Get an API Gateway Model by its rest API ID and model name: rest-api-id/model-name", + SearchDescription: "Search for API Gateway Models by their rest API ID: rest-api-id", + }, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_model.id"}, + }, +}) diff --git a/adapters/apigateway-model_test.go b/adapters/apigateway-model_test.go new file mode 100644 index 00000000..119e5312 --- /dev/null +++ b/adapters/apigateway-model_test.go @@ -0,0 +1,58 @@ +package adapters + +import ( + "github.com/overmindtech/sdp-go" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" +) + +func TestModelOutputMapper(t *testing.T) { + awsItem := &types.Model{ + Id: aws.String("model-id"), + Name: aws.String("model-name"), + Description: aws.String("description"), + Schema: aws.String("{\"type\": \"object\"}"), + ContentType: aws.String("application/json"), + } + + item, err := modelOutputMapper("rest-api-id/model-name", "scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayModelAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayModelAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-resource.go b/adapters/apigateway-resource.go index 43f56994..a37fc1ec 100644 --- a/adapters/apigateway-resource.go +++ b/adapters/apigateway-resource.go @@ -73,6 +73,20 @@ func resourceOutputMapper(query, scope string, awsItem *types.Resource) (*sdp.It } } + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: restApiID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + return &item, nil } diff --git a/adapters/apigateway-resource_test.go b/adapters/apigateway-resource_test.go index e95e34c8..5aca6b93 100644 --- a/adapters/apigateway-resource_test.go +++ b/adapters/apigateway-resource_test.go @@ -1,6 +1,7 @@ package adapters import ( + "github.com/overmindtech/sdp-go" "testing" "time" @@ -152,7 +153,7 @@ func TestResourceOutputMapper(t *testing.T) { }, } - item, err := resourceOutputMapper("rest-api-13", "scope", resource) + item, err := resourceOutputMapper("rest-api-id", "scope", resource) if err != nil { t.Fatal(err) } @@ -160,6 +161,17 @@ func TestResourceOutputMapper(t *testing.T) { if err := item.Validate(); err != nil { t.Error(err) } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) } func TestNewAPIGatewayResourceAdapter(t *testing.T) { diff --git a/adapters/apigateway-stage.go b/adapters/apigateway-stage.go new file mode 100644 index 00000000..334ef1f8 --- /dev/null +++ b/adapters/apigateway-stage.go @@ -0,0 +1,171 @@ +package adapters + +import ( + "context" + "fmt" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +func convertGetStageOutputToStage(output *apigateway.GetStageOutput) *types.Stage { + return &types.Stage{ + DeploymentId: output.DeploymentId, + StageName: output.StageName, + Description: output.Description, + CreatedDate: output.CreatedDate, + LastUpdatedDate: output.LastUpdatedDate, + Variables: output.Variables, + AccessLogSettings: output.AccessLogSettings, + CacheClusterEnabled: output.CacheClusterEnabled, + CacheClusterSize: output.CacheClusterSize, + CacheClusterStatus: output.CacheClusterStatus, + CanarySettings: output.CanarySettings, + ClientCertificateId: output.ClientCertificateId, + DocumentationVersion: output.DocumentationVersion, + MethodSettings: output.MethodSettings, + TracingEnabled: output.TracingEnabled, + WebAclArn: output.WebAclArn, + Tags: output.Tags, + } +} + +func stageOutputMapper(query, scope string, awsItem *types.Stage) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + // if it is `GET`, the query will be: rest-api-id/stage-name + // if it is `SEARCH`, the query will be: rest-api-id/deployment-id or rest-api-id + restAPIID := strings.Split(query, "/")[0] + + err = attributes.Set("UniqueAttribute", fmt.Sprintf("%s/%s", restAPIID, *awsItem.StageName)) + + item := sdp.Item{ + Type: "apigateway-stage", + UniqueAttribute: "StageName", + Attributes: attributes, + Scope: scope, + Tags: awsItem.Tags, + } + + if awsItem.DeploymentId != nil { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-deployment", + Method: sdp.QueryMethod_GET, + Query: fmt.Sprintf("%s/%s", restAPIID, *awsItem.DeploymentId), + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Deleting a deployment will impact the stage + In: true, + // Deleting a stage won't impact the deployment + Out: false, + }, + }) + } + + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "apigateway-rest-api", + Method: sdp.QueryMethod_GET, + Query: restAPIID, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // They are tightly coupled, so we need to propagate the blast to the linked item + In: true, + Out: true, + }, + }) + + return &item, nil +} + +func NewAPIGatewayStageAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.Stage, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.Stage, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-stage", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: stageAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.Stage, error) { + f := strings.Split(query, "/") + if len(f) != 2 { + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf("query must be in the format of: the rest-api-id/stage-name, but found: %s", query), + } + } + out, err := client.GetStage(ctx, &apigateway.GetStageInput{ + RestApiId: &f[0], + StageName: &f[1], + }) + if err != nil { + return nil, err + } + return convertGetStageOutputToStage(out), nil + }, + DisableList: true, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.Stage, error) { + f := strings.Split(query, "/") + var input *apigateway.GetStagesInput + + switch len(f) { + case 1: + input = &apigateway.GetStagesInput{ + RestApiId: &f[0], + } + case 2: + input = &apigateway.GetStagesInput{ + RestApiId: &f[0], + DeploymentId: &f[1], + } + default: + return nil, &sdp.QueryError{ + ErrorType: sdp.QueryError_NOTFOUND, + ErrorString: fmt.Sprintf( + "query must be in the format of: the rest-api-id/deployment-id or rest-api-id, but found: %s", + query, + ), + } + } + + out, err := client.GetStages(ctx, input) + if err != nil { + return nil, err + } + + var items []*types.Stage + for _, stage := range out.Item { + items = append(items, &stage) + } + + return items, nil + }, + ItemMapper: func(query, scope string, awsItem *types.Stage) (*sdp.Item, error) { + return stageOutputMapper(query, scope, awsItem) + }, + } +} + +var stageAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-stage", + DescriptiveName: "API Gateway Stage", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_CONFIGURATION, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + Search: true, + GetDescription: "Get an API Gateway Stage by its rest API ID and stage name: rest-api-id/stage-name", + SearchDescription: "Search for API Gateway Stages by their rest API ID or with rest API ID and deployment-id: rest-api-id/deployment-id", + }, + PotentialLinks: []string{"wafv2-web-acl"}, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_stage.id"}, + }, +}) diff --git a/adapters/apigateway-stage_test.go b/adapters/apigateway-stage_test.go new file mode 100644 index 00000000..c856d2aa --- /dev/null +++ b/adapters/apigateway-stage_test.go @@ -0,0 +1,79 @@ +package adapters + +import ( + "github.com/overmindtech/sdp-go" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" +) + +func TestStageOutputMapper(t *testing.T) { + awsItem := &types.Stage{ + DeploymentId: aws.String("deployment-id"), + StageName: aws.String("stage-name"), + Description: aws.String("description"), + CreatedDate: aws.Time(time.Now()), + LastUpdatedDate: aws.Time(time.Now()), + Variables: map[string]string{"key": "value"}, + AccessLogSettings: &types.AccessLogSettings{}, + CacheClusterEnabled: true, + CacheClusterSize: "0.5", + CacheClusterStatus: types.CacheClusterStatusAvailable, + CanarySettings: &types.CanarySettings{}, + ClientCertificateId: aws.String("client-cert-id"), + DocumentationVersion: aws.String("1.0"), + MethodSettings: map[string]types.MethodSetting{}, + TracingEnabled: true, + WebAclArn: aws.String("web-acl-arn"), + Tags: map[string]string{"tag-key": "tag-value"}, + } + + queries := []string{"rest-api-id/stage-name", "rest-api-id/deployment-id", "rest-api-id"} + for _, query := range queries { + item, err := stageOutputMapper(query, "scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "apigateway-deployment", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id/deployment-id", + ExpectedScope: "scope", + }, + { + ExpectedType: "apigateway-rest-api", + ExpectedMethod: sdp.QueryMethod_GET, + ExpectedQuery: "rest-api-id", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) + } +} + +func TestNewAPIGatewayStageAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayStageAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + SkipList: true, + } + + test.Run(t) +} diff --git a/adapters/apigateway-vpc-link.go b/adapters/apigateway-vpc-link.go new file mode 100644 index 00000000..64d679d1 --- /dev/null +++ b/adapters/apigateway-vpc-link.go @@ -0,0 +1,138 @@ +package adapters + +import ( + "context" + "strings" + + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +// convertGetVpcLinkOutputToVpcLink converts a GetVpcLinkOutput to a VpcLink +func convertGetVpcLinkOutputToVpcLink(output *apigateway.GetVpcLinkOutput) *types.VpcLink { + return &types.VpcLink{ + Id: output.Id, + Name: output.Name, + Description: output.Description, + TargetArns: output.TargetArns, + Status: output.Status, + Tags: output.Tags, + } +} + +func vpcLinkListFunc(ctx context.Context, client *apigateway.Client, _ string) ([]*types.VpcLink, error) { + out, err := client.GetVpcLinks(ctx, &apigateway.GetVpcLinksInput{}) + if err != nil { + return nil, err + } + + var items []*types.VpcLink + for _, vpcLink := range out.Items { + items = append(items, &vpcLink) + } + + return items, nil +} + +func vpcLinkOutputMapper(scope string, awsItem *types.VpcLink) (*sdp.Item, error) { + attributes, err := adapterhelpers.ToAttributesWithExclude(awsItem, "tags") + if err != nil { + return nil, err + } + + item := sdp.Item{ + Type: "apigateway-vpc-link", + UniqueAttribute: "Id", + Attributes: attributes, + Scope: scope, + Tags: awsItem.Tags, + } + + // The status of the VPC link. The valid values are AVAILABLE , PENDING , DELETING , or FAILED. + switch awsItem.Status { + case types.VpcLinkStatusAvailable: + item.Health = sdp.Health_HEALTH_OK.Enum() + case types.VpcLinkStatusPending: + item.Health = sdp.Health_HEALTH_PENDING.Enum() + case types.VpcLinkStatusDeleting: + item.Health = sdp.Health_HEALTH_PENDING.Enum() + case types.VpcLinkStatusFailed: + item.Health = sdp.Health_HEALTH_ERROR.Enum() + } + + for _, targetArn := range awsItem.TargetArns { + item.LinkedItemQueries = append(item.LinkedItemQueries, &sdp.LinkedItemQuery{ + Query: &sdp.Query{ + Type: "elbv2-load-balancer", + Method: sdp.QueryMethod_SEARCH, + Query: targetArn, + Scope: scope, + }, + BlastPropagation: &sdp.BlastPropagation{ + // Any change on the load balancer will affect the VPC link + In: true, + // Any change on the VPC link won't affect the load balancer + Out: false, + }, + }) + } + + return &item, nil +} + +func NewAPIGatewayVpcLinkAdapter(client *apigateway.Client, accountID string, region string) *adapterhelpers.GetListAdapter[*types.VpcLink, *apigateway.Client, *apigateway.Options] { + return &adapterhelpers.GetListAdapter[*types.VpcLink, *apigateway.Client, *apigateway.Options]{ + ItemType: "apigateway-vpc-link", + Client: client, + AccountID: accountID, + Region: region, + AdapterMetadata: vpcLinkAdapterMetadata, + GetFunc: func(ctx context.Context, client *apigateway.Client, scope, query string) (*types.VpcLink, error) { + out, err := client.GetVpcLink(ctx, &apigateway.GetVpcLinkInput{ + VpcLinkId: &query, + }) + if err != nil { + return nil, err + } + return convertGetVpcLinkOutputToVpcLink(out), nil + }, + ListFunc: vpcLinkListFunc, + SearchFunc: func(ctx context.Context, client *apigateway.Client, scope string, query string) ([]*types.VpcLink, error) { + out, err := client.GetVpcLinks(ctx, &apigateway.GetVpcLinksInput{}) + if err != nil { + return nil, err + } + + var items []*types.VpcLink + for _, vpcLink := range out.Items { + if strings.Contains(*vpcLink.Name, query) { + items = append(items, &vpcLink) + } + } + + return items, nil + }, + ItemMapper: func(_, scope string, awsItem *types.VpcLink) (*sdp.Item, error) { + return vpcLinkOutputMapper(scope, awsItem) + }, + } +} + +var vpcLinkAdapterMetadata = Metadata.Register(&sdp.AdapterMetadata{ + Type: "apigateway-vpc-link", + DescriptiveName: "VPC Link", + Category: sdp.AdapterCategory_ADAPTER_CATEGORY_NETWORK, + SupportedQueryMethods: &sdp.AdapterSupportedQueryMethods{ + Get: true, + List: true, + Search: true, + GetDescription: "Get a VPC Link by ID", + ListDescription: "List all VPC Links", + SearchDescription: "Search for VPC Links by their name", + }, + TerraformMappings: []*sdp.TerraformMapping{ + {TerraformQueryMap: "aws_api_gateway_vpc_link.id"}, + }, +}) diff --git a/adapters/apigateway-vpc-link_test.go b/adapters/apigateway-vpc-link_test.go new file mode 100644 index 00000000..94f6327a --- /dev/null +++ b/adapters/apigateway-vpc-link_test.go @@ -0,0 +1,58 @@ +package adapters + +import ( + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/apigateway" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" + "github.com/overmindtech/aws-source/adapterhelpers" + "github.com/overmindtech/sdp-go" +) + +func TestVpcLinkOutputMapper(t *testing.T) { + awsItem := &types.VpcLink{ + Id: aws.String("vpc-link-id"), + Name: aws.String("vpc-link-name"), + Description: aws.String("vpc-link-description"), + TargetArns: []string{"arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188"}, + Status: types.VpcLinkStatusAvailable, + Tags: map[string]string{"key": "value"}, + } + + item, err := vpcLinkOutputMapper("scope", awsItem) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if err := item.Validate(); err != nil { + t.Error(err) + } + + tests := adapterhelpers.QueryTests{ + { + ExpectedType: "elbv2-load-balancer", + ExpectedMethod: sdp.QueryMethod_SEARCH, + ExpectedQuery: "arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188", + ExpectedScope: "scope", + }, + } + + tests.Execute(t, item) +} + +func TestNewAPIGatewayVpcLinkAdapter(t *testing.T) { + config, account, region := adapterhelpers.GetAutoConfig(t) + + client := apigateway.NewFromConfig(config) + + adapter := NewAPIGatewayVpcLinkAdapter(client, account, region) + + test := adapterhelpers.E2ETest{ + Adapter: adapter, + Timeout: 10 * time.Second, + } + + test.Run(t) +} diff --git a/adapters/integration/apigateway/apigateway_test.go b/adapters/integration/apigateway/apigateway_test.go index f547848b..eb9d6f67 100644 --- a/adapters/integration/apigateway/apigateway_test.go +++ b/adapters/integration/apigateway/apigateway_test.go @@ -29,6 +29,8 @@ func APIGateway(t *testing.T) { t.Log("Running APIGateway integration test") + // Resources ------------------------------------------------------------------------------------------------------ + restApiSource := adapters.NewAPIGatewayRestApiAdapter(testClient, accountID, testAWSConfig.Region) err = restApiSource.Validate() @@ -57,6 +59,50 @@ func APIGateway(t *testing.T) { t.Fatalf("failed to validate APIGateway method response adapter: %v", err) } + integrationSource := adapters.NewAPIGatewayIntegrationAdapter(testClient, accountID, testAWSConfig.Region) + + err = integrationSource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway integration adapter: %v", err) + } + + apiKeySource := adapters.NewAPIGatewayApiKeyAdapter(testClient, accountID, testAWSConfig.Region) + + err = apiKeySource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway API key adapter: %v", err) + } + + authorizerSource := adapters.NewAPIGatewayAuthorizerAdapter(testClient, accountID, testAWSConfig.Region) + + err = authorizerSource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway authorizer adapter: %v", err) + } + + deploymentSource := adapters.NewAPIGatewayDeploymentAdapter(testClient, accountID, testAWSConfig.Region) + + err = deploymentSource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway deployment adapter: %v", err) + } + + stageSource := adapters.NewAPIGatewayStageAdapter(testClient, accountID, testAWSConfig.Region) + + err = stageSource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway stage adapter: %v", err) + } + + modelSource := adapters.NewAPIGatewayModelAdapter(testClient, accountID, testAWSConfig.Region) + + err = modelSource.Validate() + if err != nil { + t.Fatalf("failed to validate APIGateway model adapter: %v", err) + } + + // Tests ---------------------------------------------------------------------------------------------------------- + scope := adapterhelpers.FormatScope(accountID, testAWSConfig.Region) // List restApis @@ -203,5 +249,344 @@ func APIGateway(t *testing.T) { t.Fatalf("expected method response ID %s, got %s", methodResponseID, uniqueMethodResponseAttr) } + // Get integration + integrationID := fmt.Sprintf("%s/GET", resourceUniqueAttrFromGet) // resourceUniqueAttribute contains the restApiID + itgr, err := integrationSource.Get(ctx, scope, integrationID, true) + if err != nil { + t.Fatalf("failed to get APIGateway itgr: %v", err) + } + + uniqueIntegrationAttr, err := itgr.GetAttributes().Get(itgr.GetUniqueAttribute()) + if err != nil { + t.Fatalf("failed to get unique itgr attribute: %v", err) + } + + if uniqueIntegrationAttr != integrationID { + t.Fatalf("expected integration ID %s, got %s", integrationID, uniqueIntegrationAttr) + } + + // List API keys + apiKeys, err := apiKeySource.List(ctx, scope, true) + if err != nil { + t.Fatalf("failed to list APIGateway API keys: %v", err) + } + + if len(apiKeys) == 0 { + t.Fatalf("no API keys found") + } + + apiKeyUniqueAttribute := apiKeys[0].GetUniqueAttribute() + + apiKeyID, err := integration.GetUniqueAttributeValueByTags( + apiKeyUniqueAttribute, + apiKeys, + integration.ResourceTags(integration.APIGateway, apiKeySrc), + true, + ) + if err != nil { + t.Fatalf("failed to get API key ID: %v", err) + } + + // Get API key + apiKey, err := apiKeySource.Get(ctx, scope, apiKeyID, true) + if err != nil { + t.Fatalf("failed to get APIGateway API key: %v", err) + } + + apiKeyIDFromGet, err := integration.GetUniqueAttributeValueByTags( + apiKeyUniqueAttribute, + []*sdp.Item{apiKey}, + integration.ResourceTags(integration.APIGateway, apiKeySrc), + true, + ) + if err != nil { + t.Fatalf("failed to get API key ID from get: %v", err) + } + + if apiKeyID != apiKeyIDFromGet { + t.Fatalf("expected API key ID %s, got %s", apiKeyID, apiKeyIDFromGet) + } + + // Search API keys + apiKeyName := integration.ResourceName(integration.APIGateway, apiKeySrc, integration.TestID()) + apiKeysFromSearch, err := apiKeySource.Search(ctx, scope, apiKeyName, true) + if err != nil { + t.Fatalf("failed to search APIGateway API keys: %v", err) + } + + if len(apiKeysFromSearch) == 0 { + t.Fatalf("no API keys found") + } + + apiKeyIDFromSearch, err := integration.GetUniqueAttributeValueBySignificantAttribute( + apiKeyUniqueAttribute, + "Name", + apiKeyName, + apiKeysFromSearch, + true, + ) + if err != nil { + t.Fatalf("failed to get API key ID from search: %v", err) + } + + if apiKeyID != apiKeyIDFromSearch { + t.Fatalf("expected API key ID %s, got %s", apiKeyID, apiKeyIDFromSearch) + } + + // Search authorizers by restApiID + authorizers, err := authorizerSource.Search(ctx, scope, restApiID, true) + if err != nil { + t.Fatalf("failed to search APIGateway authorizers: %v", err) + } + + authorizerUniqueAttribute := authorizers[0].GetUniqueAttribute() + + authorizerTestName := integration.ResourceName(integration.APIGateway, authorizerSrc, integration.TestID()) + authorizerID, err := integration.GetUniqueAttributeValueBySignificantAttribute( + authorizerUniqueAttribute, + "Name", + authorizerTestName, + authorizers, + true, + ) + if err != nil { + t.Fatalf("failed to get authorizer ID: %v", err) + } + + // Get authorizer + query := fmt.Sprintf("%s/%s", restApiID, authorizerID) + authorizer, err := authorizerSource.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get APIGateway authorizer: %v", err) + } + + authorizerIDFromGet, err := integration.GetUniqueAttributeValueBySignificantAttribute( + authorizerUniqueAttribute, + "Name", + authorizerTestName, + []*sdp.Item{authorizer}, + true, + ) + if err != nil { + t.Fatalf("failed to get authorizer ID from get: %v", err) + } + + if authorizerID != authorizerIDFromGet { + t.Fatalf("expected authorizer ID %s, got %s", authorizerID, authorizerIDFromGet) + } + + // Search authorizer by restApiID/name + query = fmt.Sprintf("%s/%s", restApiID, authorizerTestName) + authorizersFromSearch, err := authorizerSource.Search(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to search APIGateway authorizers: %v", err) + } + + if len(authorizersFromSearch) == 0 { + t.Fatalf("no authorizers found") + } + + authorizerIDFromSearch, err := integration.GetUniqueAttributeValueBySignificantAttribute( + authorizerUniqueAttribute, + "Name", + authorizerTestName, + authorizersFromSearch, + true, + ) + if err != nil { + t.Fatalf("failed to get authorizer ID from search: %v", err) + } + + if authorizerID != authorizerIDFromSearch { + t.Fatalf("expected authorizer ID %s, got %s", authorizerID, authorizerIDFromSearch) + } + + // Search deployments by restApiID + deployments, err := deploymentSource.Search(ctx, scope, restApiID, true) + if err != nil { + t.Fatalf("failed to search APIGateway deployments: %v", err) + } + + if len(deployments) == 0 { + t.Fatalf("no deployments found") + } + + deploymentUniqueAttribute := deployments[0].GetUniqueAttribute() + + deploymentID, err := integration.GetUniqueAttributeValueBySignificantAttribute( + deploymentUniqueAttribute, + "Description", + "test-deployment", + deployments, + true, + ) + if err != nil { + t.Fatalf("failed to get deployment ID: %v", err) + } + + // Get deployment + query = fmt.Sprintf("%s/%s", restApiID, deploymentID) + deployment, err := deploymentSource.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get APIGateway deployment: %v", err) + } + + deploymentIDFromGet, err := integration.GetUniqueAttributeValueBySignificantAttribute( + deploymentUniqueAttribute, + "Description", + "test-deployment", + []*sdp.Item{deployment}, + true, + ) + if err != nil { + t.Fatalf("failed to get deployment ID from get: %v", err) + } + + if deploymentID != deploymentIDFromGet { + t.Fatalf("expected deployment ID %s, got %s", deploymentID, deploymentIDFromGet) + } + + // Search deployment by restApiID/description + query = fmt.Sprintf("%s/test-deployment", restApiID) + deploymentsFromSearch, err := deploymentSource.Search(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to search APIGateway deployments: %v", err) + } + + if len(deploymentsFromSearch) == 0 { + t.Fatalf("no deployments found") + } + + deploymentIDFromSearch, err := integration.GetUniqueAttributeValueBySignificantAttribute( + deploymentUniqueAttribute, + "Description", + "test-deployment", + deploymentsFromSearch, + true, + ) + if err != nil { + t.Fatalf("failed to get deployment ID from search: %v", err) + } + + if deploymentID != deploymentIDFromSearch { + t.Fatalf("expected deployment ID %s, got %s", deploymentID, deploymentIDFromSearch) + } + + // Search stages by restApiID + stages, err := stageSource.Search(ctx, scope, restApiID, true) + if err != nil { + t.Fatalf("failed to search APIGateway stages: %v", err) + } + + if len(stages) == 0 { + t.Fatalf("no stages found") + } + + stageUniqueAttribute := stages[0].GetUniqueAttribute() + + stageID, err := integration.GetUniqueAttributeValueBySignificantAttribute( + stageUniqueAttribute, + "StageName", + "dev", + stages, + true, + ) + if err != nil { + t.Fatalf("failed to get stage ID: %v", err) + } + + // Get stage + query = fmt.Sprintf("%s/dev", restApiID) + stage, err := stageSource.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get APIGateway stage: %v", err) + } + + stageIDFromGet, err := integration.GetUniqueAttributeValueBySignificantAttribute( + stageUniqueAttribute, + "StageName", + "dev", + []*sdp.Item{stage}, + true, + ) + if err != nil { + t.Fatalf("failed to get stage ID from get: %v", err) + } + + if stageID != stageIDFromGet { + t.Fatalf("expected stage ID %s, got %s", stageID, stageIDFromGet) + } + + // Search stage by restApiID/deploymentID + query = fmt.Sprintf("%s/%s", restApiID, deploymentID) + stagesFromSearch, err := stageSource.Search(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to search APIGateway stages: %v", err) + } + + if len(stagesFromSearch) == 0 { + t.Fatalf("no stages found") + } + + stageIDFromSearch, err := integration.GetUniqueAttributeValueBySignificantAttribute( + stageUniqueAttribute, + "StageName", + "dev", + stagesFromSearch, + true, + ) + if err != nil { + t.Fatalf("failed to get stage ID from search: %v", err) + } + + if stageID != stageIDFromSearch { + t.Fatalf("expected stage ID %s, got %s", stageID, stageIDFromSearch) + } + + // Search models by restApiID + models, err := modelSource.Search(ctx, scope, restApiID, true) + if err != nil { + t.Fatalf("failed to search APIGateway models: %v", err) + } + + if len(models) == 0 { + t.Fatalf("no models found") + } + + modelUniqueAttribute := models[0].GetUniqueAttribute() + + modelID, err := integration.GetUniqueAttributeValueBySignificantAttribute( + modelUniqueAttribute, + "Name", + "testModel", + models, + true, + ) + if err != nil { + t.Fatalf("failed to get model ID: %v", err) + } + + // Get model + query = fmt.Sprintf("%s/testModel", restApiID) + model, err := modelSource.Get(ctx, scope, query, true) + if err != nil { + t.Fatalf("failed to get APIGateway model: %v", err) + } + + modelIDFromGet, err := integration.GetUniqueAttributeValueBySignificantAttribute( + modelUniqueAttribute, + "Name", + "testModel", + []*sdp.Item{model}, + true, + ) + if err != nil { + t.Fatalf("failed to get model ID from get: %v", err) + } + + if modelID != modelIDFromGet { + t.Fatalf("expected model ID %s, got %s", modelID, modelIDFromGet) + } + t.Log("APIGateway integration test completed") } diff --git a/adapters/integration/apigateway/create.go b/adapters/integration/apigateway/create.go index 94517051..2851e0d1 100644 --- a/adapters/integration/apigateway/create.go +++ b/adapters/integration/apigateway/create.go @@ -3,6 +3,7 @@ package apigateway import ( "context" "errors" + "github.com/aws/aws-sdk-go-v2/service/apigateway/types" "log/slog" "strings" @@ -139,3 +140,178 @@ func createMethodResponse(ctx context.Context, logger *slog.Logger, client *apig return nil } + +func createIntegration(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID, resourceID *string, method string) error { + // check if an integration with the same method already exists + err := findIntegration(ctx, client, restAPIID, resourceID, method) + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating integration") + } else { + return err + } + } + + if err == nil { + logger.InfoContext(ctx, "Integration already exists") + return nil + } + + _, err = client.PutIntegration(ctx, &apigateway.PutIntegrationInput{ + RestApiId: restAPIID, + ResourceId: resourceID, + HttpMethod: adapterhelpers.PtrString(method), + Type: "MOCK", + }) + if err != nil { + return err + } + + return nil +} + +func createAPIKey(ctx context.Context, logger *slog.Logger, client *apigateway.Client, testID string) error { + // check if an API key with the same name already exists + id, err := findAPIKeyByName(ctx, client, integration.ResourceName(integration.APIGateway, apiKeySrc, testID)) + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating API key") + } else { + return err + } + } + + if id != nil { + logger.InfoContext(ctx, "API key already exists") + return nil + } + + _, err = client.CreateApiKey(ctx, &apigateway.CreateApiKeyInput{ + Name: adapterhelpers.PtrString(integration.ResourceName(integration.APIGateway, apiKeySrc, testID)), + Tags: resourceTags(apiKeySrc, testID), + Enabled: true, + }) + if err != nil { + return err + } + + return nil +} + +func createAuthorizer(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID, testID string) error { + // check if an authorizer with the same name already exists + id, err := findAuthorizerByName(ctx, client, restAPIID, integration.ResourceName(integration.APIGateway, authorizerSrc, testID)) + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating authorizer") + } else { + return err + } + } + + if id != nil { + logger.InfoContext(ctx, "Authorizer already exists") + return nil + } + + identitySource := "method.request.header.Authorization" + _, err = client.CreateAuthorizer(ctx, &apigateway.CreateAuthorizerInput{ + RestApiId: &restAPIID, + Name: adapterhelpers.PtrString(integration.ResourceName(integration.APIGateway, authorizerSrc, testID)), + Type: types.AuthorizerTypeToken, + IdentitySource: &identitySource, + AuthorizerUri: adapterhelpers.PtrString("arn:aws:apigateway:us-east-1:lambda:path/2015-03-31/functions/arn:aws:lambda:us-east-1:123456789012:function:auth-function/invocations"), + }) + if err != nil { + return err + } + + return nil +} + +func createDeployment(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID string) (*string, error) { + // check if a deployment with the same name already exists + id, err := findDeploymentByDescription(ctx, client, restAPIID, "test-deployment") + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating deployment") + } else { + return nil, err + } + } + + if id != nil { + logger.InfoContext(ctx, "Deployment already exists") + return id, nil + } + + resp, err := client.CreateDeployment(ctx, &apigateway.CreateDeploymentInput{ + RestApiId: &restAPIID, + Description: adapterhelpers.PtrString("test-deployment"), + }) + if err != nil { + return nil, err + } + + return resp.Id, nil +} + +func createStage(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID, deploymentID string) error { + // check if a stage with the same name already exists + stgName := "dev" + err := findStageByName(ctx, client, restAPIID, stgName) + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating stage") + } else { + return err + } + } + + if err == nil { + logger.InfoContext(ctx, "Stage already exists") + return nil + } + + _, err = client.CreateStage(ctx, &apigateway.CreateStageInput{ + RestApiId: &restAPIID, + DeploymentId: &deploymentID, + StageName: &stgName, + }) + if err != nil { + return err + } + + return nil +} + +func createModel(ctx context.Context, logger *slog.Logger, client *apigateway.Client, restAPIID string) error { + modelName := "testModel" + + // check if a model with the same testID already exists + err := findModelByName(ctx, client, restAPIID, modelName) + if err != nil { + if errors.As(err, new(integration.NotFoundError)) { + logger.InfoContext(ctx, "Creating model") + } else { + return err + } + } + + if err == nil { + logger.InfoContext(ctx, "Model already exists") + return nil + } + + _, err = client.CreateModel(ctx, &apigateway.CreateModelInput{ + RestApiId: &restAPIID, + Name: &modelName, + Schema: adapterhelpers.PtrString("{}"), + ContentType: adapterhelpers.PtrString("application/json"), + }) + if err != nil { + return err + } + + return nil +} diff --git a/adapters/integration/apigateway/delete.go b/adapters/integration/apigateway/delete.go index 16222eb3..e8008585 100644 --- a/adapters/integration/apigateway/delete.go +++ b/adapters/integration/apigateway/delete.go @@ -2,7 +2,6 @@ package apigateway import ( "context" - "github.com/aws/aws-sdk-go-v2/service/apigateway" "github.com/overmindtech/aws-source/adapterhelpers" ) @@ -14,3 +13,14 @@ func deleteRestAPI(ctx context.Context, client *apigateway.Client, restAPIID str return err } + +func deleteAPIKeyByName(ctx context.Context, client *apigateway.Client, id *string) error { + _, err := client.DeleteApiKey(ctx, &apigateway.DeleteApiKeyInput{ + ApiKey: id, + }) + if err != nil { + return err + } + + return nil +} diff --git a/adapters/integration/apigateway/find.go b/adapters/integration/apigateway/find.go index cf146509..3fa47681 100644 --- a/adapters/integration/apigateway/find.go +++ b/adapters/integration/apigateway/find.go @@ -87,3 +87,143 @@ func findMethodResponse(ctx context.Context, client *apigateway.Client, restAPII return nil } + +func findIntegration(ctx context.Context, client *apigateway.Client, restAPIID, resourceID *string, method string) error { + _, err := client.GetIntegration(ctx, &apigateway.GetIntegrationInput{ + RestApiId: restAPIID, + ResourceId: resourceID, + HttpMethod: &method, + }) + + if err != nil { + var notFoundErr *types.NotFoundException + if errors.As(err, ¬FoundErr) { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + integrationSrc, + method, + )) + } + + return err + } + + return nil +} + +func findAPIKeyByName(ctx context.Context, client *apigateway.Client, name string) (*string, error) { + result, err := client.GetApiKeys(ctx, &apigateway.GetApiKeysInput{ + NameQuery: &name, + }) + if err != nil { + return nil, err + } + + if len(result.Items) == 0 { + return nil, integration.NewNotFoundError(integration.ResourceName(integration.APIGateway, apiKeySrc, name)) + } + + for _, apiKey := range result.Items { + if *apiKey.Name == name { + return apiKey.Id, nil + } + } + + return nil, integration.NewNotFoundError(integration.ResourceName(integration.APIGateway, apiKeySrc, name)) +} + +func findAuthorizerByName(ctx context.Context, client *apigateway.Client, restAPIID, name string) (*string, error) { + result, err := client.GetAuthorizers(ctx, &apigateway.GetAuthorizersInput{ + RestApiId: &restAPIID, + }) + if err != nil { + return nil, err + } + + if len(result.Items) == 0 { + return nil, integration.NewNotFoundError(integration.ResourceName(integration.APIGateway, authorizerSrc, name)) + } + + for _, authorizer := range result.Items { + if *authorizer.Name == name { + return authorizer.Id, nil + } + } + + return nil, integration.NewNotFoundError(integration.ResourceName(integration.APIGateway, authorizerSrc, name)) +} + +func findDeploymentByDescription(ctx context.Context, client *apigateway.Client, restAPIID, description string) (*string, error) { + result, err := client.GetDeployments(ctx, &apigateway.GetDeploymentsInput{ + RestApiId: &restAPIID, + }) + if err != nil { + return nil, err + } + + for _, deployment := range result.Items { + if *deployment.Description == description { + return deployment.Id, nil + } + } + + return nil, integration.NewNotFoundError(integration.ResourceName(integration.APIGateway, deploymentSrc, description)) +} + +func findStageByName(ctx context.Context, client *apigateway.Client, restAPIID, name string) error { + result, err := client.GetStage(ctx, &apigateway.GetStageInput{ + RestApiId: &restAPIID, + StageName: &name, + }) + if err != nil { + var notFoundErr *types.NotFoundException + if errors.As(err, ¬FoundErr) { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + stageSrc, + name, + )) + } + + return err + } + + if result == nil { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + stageSrc, + name, + )) + } + + return nil +} + +func findModelByName(ctx context.Context, client *apigateway.Client, restAPIID, name string) error { + result, err := client.GetModel(ctx, &apigateway.GetModelInput{ + RestApiId: &restAPIID, + ModelName: &name, + }) + if err != nil { + var notFoundErr *types.NotFoundException + if errors.As(err, ¬FoundErr) { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + stageSrc, + name, + )) + } + + return err + } + + if result == nil { + return integration.NewNotFoundError(integration.ResourceName( + integration.APIGateway, + stageSrc, + name, + )) + } + + return nil +} diff --git a/adapters/integration/apigateway/setup.go b/adapters/integration/apigateway/setup.go index 46f29b6f..36a5a376 100644 --- a/adapters/integration/apigateway/setup.go +++ b/adapters/integration/apigateway/setup.go @@ -13,6 +13,12 @@ const ( resourceSrc = "resource" methodSrc = "method" methodResponseSrc = "method-response" + integrationSrc = "integration" + apiKeySrc = "api-key" + authorizerSrc = "authorizer" + deploymentSrc = "deployment" + stageSrc = "stage" + modelSrc = "model" ) func setup(ctx context.Context, logger *slog.Logger, client *apigateway.Client) error { @@ -48,5 +54,41 @@ func setup(ctx context.Context, logger *slog.Logger, client *apigateway.Client) return err } + // Create integration + err = createIntegration(ctx, logger, client, restApiID, testResourceID, "GET") + if err != nil { + return err + } + + // Create API Key + err = createAPIKey(ctx, logger, client, testID) + if err != nil { + return err + } + + // Create Authorizer + err = createAuthorizer(ctx, logger, client, *restApiID, testID) + if err != nil { + return err + } + + // Create Deployment + deploymentID, err := createDeployment(ctx, logger, client, *restApiID) + if err != nil { + return err + } + + // Create Stage + err = createStage(ctx, logger, client, *restApiID, *deploymentID) + if err != nil { + return err + } + + // Create Model + err = createModel(ctx, logger, client, *restApiID) + if err != nil { + return err + } + return nil } diff --git a/adapters/integration/apigateway/teardown.go b/adapters/integration/apigateway/teardown.go index 247f2f64..30e4be85 100644 --- a/adapters/integration/apigateway/teardown.go +++ b/adapters/integration/apigateway/teardown.go @@ -14,11 +14,31 @@ func teardown(ctx context.Context, logger *slog.Logger, client *apigateway.Clien if err != nil { if nf := integration.NewNotFoundError(restAPISrc); errors.As(err, &nf) { logger.WarnContext(ctx, "Rest API not found") + } else { + return err + } + } else { + err = deleteRestAPI(ctx, client, *restAPIID) + if err != nil { + return err + } + } + + keyName := integration.ResourceName(integration.APIGateway, apiKeySrc, integration.TestID()) + apiKeyID, err := findAPIKeyByName(ctx, client, keyName) + if err != nil { + if nf := integration.NewNotFoundError(apiKeySrc); errors.As(err, &nf) { + logger.WarnContext(ctx, "API Key not found", "name", keyName) return nil } else { return err } + } else { + err = deleteAPIKeyByName(ctx, client, apiKeyID) + if err != nil { + return err + } } - return deleteRestAPI(ctx, client, *restAPIID) + return nil } diff --git a/proc/proc.go b/proc/proc.go index b1b1dec1..e490ef87 100644 --- a/proc/proc.go +++ b/proc/proc.go @@ -481,6 +481,14 @@ func InitializeAwsSourceEngine(ctx context.Context, ec *discovery.EngineConfig, adapters.NewAPIGatewayDomainNameAdapter(apigatewayClient, *callerID.Account, cfg.Region), adapters.NewAPIGatewayMethodAdapter(apigatewayClient, *callerID.Account, cfg.Region), adapters.NewAPIGatewayMethodResponseAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayIntegrationAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayVpcLinkAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayApiKeyAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayAuthorizerAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayDeploymentAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayStageAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayModelAdapter(apigatewayClient, *callerID.Account, cfg.Region), + adapters.NewAPIGatewayBasePathMappingAdapter(apigatewayClient, *callerID.Account, cfg.Region), // SSM adapters.NewSSMParameterAdapter(ssmClient, *callerID.Account, cfg.Region),