diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index fedf1add..68d63d43 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -25,12 +25,40 @@ jobs: uses: ./.github/workflows/maven.yml if: github.repository_owner == 'epam' + check-modified-files: + runs-on: ubuntu-latest + outputs: + files_modified: ${{ steps.check_files.outputs.run_publish_steps }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + - name: Check modified files + id: check_files + run: | + git fetch origin master + changed_files=$(git diff --name-only origin/master) + echo "Changed files: ${changed_files}" + checked_directory="plugin/" + for file in ${changed_files} + do + if [[ ${file} == ${checked_directory}* ]] + then + echo "Target directory was modified." + echo "run_publish_steps=true" >>$GITHUB_OUTPUT + exit 0 + fi + done + echo "Target directory was not modified." + echo "run_publish_steps=false" >>$GITHUB_OUTPUT + echo "dist=/tmp/bavp/dist" >>$GITHUB_OUTPUT + shell: bash + release-snapshot-to-maven-central: name: Publish SNAPSHOT package to MavenCentral environment: release-snapshot - if: github.repository_owner == 'epam' && github.event_name == 'push' && github.ref == 'refs/heads/master' + needs: [maven, check-modified-files] runs-on: ubuntu-latest - needs: maven + if: github.repository_owner == 'epam' && github.event_name == 'push' && github.ref == 'refs/heads/master' && needs.check-modified-files.outputs.files_modified == 'true' steps: - uses: actions/checkout@v4 @@ -46,6 +74,7 @@ jobs: settings-path: ${{ github.workspace }} - name: Set SNAPSHOT in version run: | + gpg --version syndicate_plugin_version=$(mvn help:evaluate -Dexpression=project.version -q -DforceStdout --file ./plugin/pom.xml | xargs) mvn versions:set -DnewVersion="${syndicate_plugin_version}-SNAPSHOT" --file ./plugin/pom.xml mvn versions:commit --file ./plugin/pom.xml @@ -53,7 +82,7 @@ jobs: run: mvn -B package --file ./plugin/pom.xml - name: Deploy development version binaries (Snapshots) env: - OSS_SONATYPE_USERNAME: ${{ vars.OSSRH_USERNAME }} + OSS_SONATYPE_USERNAME: ${{ secrets.OSSRH_USERNAME }} OSS_SONATYPE_TOKEN: ${{ secrets.OSSRH_TOKEN }} run: | echo Checking variables ${{ secrets.OSSRH_USERNAME }} @@ -64,9 +93,9 @@ jobs: release-to-maven-central: name: Publish released package to MavenCentral environment: release-maven-central - if: github.repository_owner == 'epam' && github.event.action == 'published' + needs: [maven, check-modified-files] runs-on: ubuntu-latest - needs: maven + if: github.repository_owner == 'epam' && github.event.action == 'published' && needs.check-modified-files.outputs.files_modified == 'true' steps: - uses: actions/checkout@v4 @@ -88,6 +117,7 @@ jobs: # -U force updates just to make sure we are using latest dependencies # -B Batch mode (do not ask for user input), just in case # -P activate profile + gpg --version mvn -U -B clean deploy -P release --file ./plugin/pom.xml env: GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/CHANGELOG.md b/CHANGELOG.md index f9cfc7b5..65c3ac1c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +# [1.13.0] - 2024-07-10 +- Added possibility to configure `FunctionResponseTypes` for lambda functions +- Updated maven plugin version to 1.12.0 with support of `FunctionResponseTypes` +- Added possibility to set up Cognito user pool ID in lambda function environment variable +- Added possibility to set up Cognito user pool client ID in lambda function environment variable +- Fix lambda triggers deletion when removed from meta +- Fix resources dependencies resolving +- Fix losing successfully deployed resources from the output file during deployment with the option `--continue_deploy` +- Fix API Gateway duplication in case of existing API Gateway with the same name +- Fix detection of usage `--rollback_on_error` option with an incompatible option `--continue_deploy` +- Changed datetime format for lock attributes in the `.syndicate` file to UTC format +- The Syndicate Java plugin version updated to 1.13.0 with changes: + - The ResourceType enum for the @DependsOn annotation extended with new type ResourceType.COGNITO_USER_POOL + - The @EnvironmentVariable annotation for the Syndicate Java plugin improved to support the value transformer + - A new value transformer type created ValueTransformer.USER_POOL_NAME_TO_USER_POOL_ID + - A new value transformer type created ValueTransformer.USER_POOL_NAME_TO_CLIENT_ID +- The generate Java lambda template changed to use the Syndicate Java plugin version 1.13.0 + # [1.12.0] - 2024-06-20 - Added ability for `clean` command to automatically resolve if `--rollback` is needed. - Fixed an issue related to `log group already exists` error while deploying or updating `lambda`. diff --git a/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate.yml b/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate.yml new file mode 100644 index 00000000..ecb3aa3d --- /dev/null +++ b/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate.yml @@ -0,0 +1,4 @@ +account_id: ACCOUNT_ID +region: REGION_NAME +deploy_target_bucket: BUCKET_NAME +project_path: PROJECT_FOLDER diff --git a/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate_aliases.yml b/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate_aliases.yml new file mode 100644 index 00000000..1ec2ac16 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/.syndicate-config-demo-apigateway-cognito/syndicate_aliases.yml @@ -0,0 +1,5 @@ +account_id: ACCOUNT_ID +region: REGION_NAME +logs_expiration: 30 + +pool_name: USERPOOL_NAME diff --git a/examples/java/demo-apigateway-cognito/README.md b/examples/java/demo-apigateway-cognito/README.md new file mode 100644 index 00000000..305a9927 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/README.md @@ -0,0 +1,35 @@ +#### This example shows a Syndicate configuration for deploying: +* 1 Java Lambda function; +* 1 IAM role attached to lambda; +* 1 Custom IAM policy attached to role; +* 1 API Gateway +* 1 Cognito User Pool + +#### To deploy this example: + +##### 1. Replace following placeholders in `syndicate.yml`: +* `ACCOUNT_ID` - AWS account id where syndicate will deploy this demo; +* `REGION_NAME` - AWS region where syndicate will deploy this demo; +* `BUCKET_NAME` - bucket name to upload deployment artifacts, must be unique across all AWS accounts; +* `PROJECT_FOLDER` - absolute path to the project folder; + +##### 2. Replace following placeholder in `syndicate_aliases.yml`: +* `ACCOUNT_ID` - AWS account id where syndicate will deploy this demo; +* `REGION_NAME` - AWS region where syndicate will deploy this demo; +* `USERPOOL_NAME` - desired Cognito User Pool name; + +##### 3. Export config files path (set environment variable SDCT_CONF): +* Unix: `export SDCT_CONF=$CONFIG_FOLDER`, in this example $CONFIG_FOLDER is PROJECT_FOLDER/.syndicate-config-demo-apigateway-cognito; +* Windows (cmd): `set SDCT_CONF=%CONFIG_FOLDER%`, in this example %CONFIG_FOLDER% is PROJECT_FOLDER/.syndicate-config-demo-apigateway-cognito; + +##### 4. Build bundle: + +`syndicate build` + +##### 5. Deploy: + +`syndicate deploy` + +#### 6. To clean project resources: + +`syndicate clean` \ No newline at end of file diff --git a/examples/java/demo-apigateway-cognito/deployment_resources.json b/examples/java/demo-apigateway-cognito/deployment_resources.json new file mode 100644 index 00000000..0f9aa778 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/deployment_resources.json @@ -0,0 +1,144 @@ +{ + "lambda-cognito-execution": { + "policy_content": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + + "cognito-idp:DescribeUserPool", + "cognito-idp:GetUser", + "cognito-idp:ListUsers", + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:GetIdentityProviderByIdentifier", + "cognito-idp:ListUserPools", + "cognito-idp:ListUserPoolClients", + "cognito-idp:AdminRespondToAuthChallenge", + + "ssm:PutParameter", + "ssm:GetParameter", + "kms:Decrypt" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "resource_type": "iam_policy" + }, + + "api-handler-role": { + "predefined_policies": [], + "principal_service": "lambda", + "custom_policies": [ + "lambda-cognito-execution" + ], + "resource_type": "iam_role" + }, + + "${pool_name}": { + "resource_type": "cognito_idp", + "password_policy": { + "minimum_length": 8, + "require_uppercase": false, + "require_symbols": false, + "require_lowercase": false, + "require_numbers": false + }, + "auto_verified_attributes": [], + "sms_configuration": {}, + "username_attributes": [], + "custom_attributes": [], + "client": { + "client_name": "client-app", + "generate_secret": false, + "explicit_auth_flows": [ + "ALLOW_ADMIN_USER_PASSWORD_AUTH", + "ALLOW_CUSTOM_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + } + }, + + "demo-api-gateway": { + "resource_type": "api_gateway", + "deploy_stage": "api", + "authorizers": { + "authorizer": { + "type": "COGNITO_USER_POOLS", + "identity_source": "method.request.header.Authorization", + "user_pools": [ + "${pool_name}" + ], + "ttl": 300 + } + }, + "resources": { + "/": { + "enable_cors": false, + "GET": { + "enable_proxy": true, + "authorization_type": "NONE", + "integration_type": "lambda", + "lambda_name": "api-handler", + "api_key_required": false, + "method_request_parameters": {}, + "integration_request_body_template": {}, + "responses": [], + "integration_responses": [], + "default_error_pattern": true + } + }, + "/secured": { + "enable_cors": false, + "GET": { + "enable_proxy": true, + "authorization_type": "authorizer", + "integration_type": "lambda", + "lambda_name": "api-handler", + "api_key_required": false, + "method_request_parameters": {}, + "integration_request_body_template": {}, + "responses": [], + "integration_responses": [], + "default_error_pattern": true + } + }, + "/signin": { + "enable_cors": false, + "POST": { + "enable_proxy": true, + "authorization_type": "NONE", + "integration_type": "lambda", + "lambda_name": "api-handler", + "api_key_required": false, + "method_request_parameters": {}, + "integration_request_body_template": {}, + "responses": [], + "integration_responses": [], + "default_error_pattern": true + } + }, + "/signup": { + "enable_cors": false, + "POST": { + "enable_proxy": true, + "authorization_type": "NONE", + "integration_type": "lambda", + "lambda_name": "api-handler", + "api_key_required": false, + "method_request_parameters": {}, + "integration_request_body_template": {}, + "responses": [], + "integration_responses": [], + "default_error_pattern": true + } + } + } + } +} \ No newline at end of file diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/ApiHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/ApiHandler.java new file mode 100644 index 00000000..f506b3b2 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/ApiHandler.java @@ -0,0 +1,111 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.demoapigatewaycognito.dto.RouteKey; +import com.demoapigatewaycognito.handler.GetRootHandler; +import com.demoapigatewaycognito.handler.GetSecuredHandler; +import com.demoapigatewaycognito.handler.RouteNotImplementedHandler; +import com.demoapigatewaycognito.handler.PostSignInHandler; +import com.demoapigatewaycognito.handler.PostSignUpHandler; +import com.syndicate.deployment.annotations.environment.EnvironmentVariable; +import com.syndicate.deployment.annotations.environment.EnvironmentVariables; +import com.syndicate.deployment.annotations.lambda.LambdaHandler; +import com.syndicate.deployment.annotations.resources.DependsOn; +import com.syndicate.deployment.model.DeploymentRuntime; +import com.syndicate.deployment.model.ResourceType; +import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; + +import java.util.Map; + +import static com.syndicate.deployment.model.environment.ValueTransformer.USER_POOL_NAME_TO_CLIENT_ID; +import static com.syndicate.deployment.model.environment.ValueTransformer.USER_POOL_NAME_TO_USER_POOL_ID; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +@DependsOn(resourceType = ResourceType.COGNITO_USER_POOL, name = "${pool_name}") +@LambdaHandler(lambdaName = "api-handler", roleName = "api-handler-role", runtime = DeploymentRuntime.JAVA17) +@EnvironmentVariables(value = { + @EnvironmentVariable(key = "REGION", value = "${region}"), + @EnvironmentVariable(key = "COGNITO_ID", value = "${pool_name}", valueTransformer = USER_POOL_NAME_TO_USER_POOL_ID), + @EnvironmentVariable(key = "CLIENT_ID", value = "${pool_name}", valueTransformer = USER_POOL_NAME_TO_CLIENT_ID) +}) +public class ApiHandler implements RequestHandler { + + private final CognitoIdentityProviderClient cognitoClient; + private final Map> handlersByRouteKey; + private final Map headersForCORS; + private final RequestHandler routeNotImplementedHandler; + + public ApiHandler() { + this.cognitoClient = initCognitoClient(); + this.handlersByRouteKey = initHandlers(); + this.headersForCORS = initHeadersForCORS(); + this.routeNotImplementedHandler = new RouteNotImplementedHandler(); + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + return getHandler(requestEvent) + .handleRequest(requestEvent, context) + .withHeaders(headersForCORS); + } + + private RequestHandler getHandler(APIGatewayProxyRequestEvent requestEvent) { + return handlersByRouteKey.getOrDefault(getRouteKey(requestEvent), routeNotImplementedHandler); + } + + private RouteKey getRouteKey(APIGatewayProxyRequestEvent requestEvent) { + return new RouteKey(requestEvent.getHttpMethod(), requestEvent.getPath()); + } + + private CognitoIdentityProviderClient initCognitoClient() { + return CognitoIdentityProviderClient.builder() + .region(Region.of(System.getenv("REGION"))) + .credentialsProvider(DefaultCredentialsProvider.create()) + .build(); + } + + private Map> initHandlers() { + return Map.of( + new RouteKey("GET", "/"), new GetRootHandler(), + new RouteKey("POST", "/signup"), new PostSignUpHandler(cognitoClient), + new RouteKey("POST", "/signin"), new PostSignInHandler(cognitoClient), + new RouteKey("GET", "/secured"), new GetSecuredHandler() + ); + } + + /** + * To allow all origins, all methods, and common headers + * Using cross-origin resource sharing (CORS) + */ + private Map initHeadersForCORS() { + return Map.of( + "Access-Control-Allow-Headers", "Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token", + "Access-Control-Allow-Origin", "*", + "Access-Control-Allow-Methods", "*", + "Accept-Version", "*" + ); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/RouteKey.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/RouteKey.java new file mode 100644 index 00000000..99b7eca2 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/RouteKey.java @@ -0,0 +1,23 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.dto; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public record RouteKey(String method, String path) { + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignIn.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignIn.java new file mode 100644 index 00000000..188cac0f --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignIn.java @@ -0,0 +1,39 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.dto; + +import org.json.JSONObject; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public record SignIn(String nickName, String password) { + + public SignIn { + if (nickName == null || password == null) { + throw new IllegalArgumentException("Missing or incomplete data."); + } + } + + public static SignIn fromJson(String jsonString) { + JSONObject json = new JSONObject(jsonString); + String nickName = json.optString("nickName", null); + String password = json.optString("password", null); + + return new SignIn(nickName, password); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignUp.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignUp.java new file mode 100644 index 00000000..245cfe3c --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/dto/SignUp.java @@ -0,0 +1,42 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.dto; + +import org.json.JSONObject; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public record SignUp(String email, String password, String firstName, String lastName, String nickName) { + + public SignUp { + if (email == null || password == null || firstName == null || lastName == null || nickName == null) { + throw new IllegalArgumentException("Missing or incomplete data."); + } + } + + public static SignUp fromJson(String jsonString) { + JSONObject json = new JSONObject(jsonString); + String email = json.optString("email", null); + String password = json.optString("password", null); + String firstName = json.optString("firstName", null); + String lastName = json.optString("lastName", null); + String nickName = json.optString("nickName", null); + + return new SignUp(email, password, firstName, lastName, nickName); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/CognitoSupport.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/CognitoSupport.java new file mode 100644 index 00000000..4fafeee6 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/CognitoSupport.java @@ -0,0 +1,112 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.demoapigatewaycognito.dto.SignUp; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminCreateUserRequest; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminCreateUserResponse; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminInitiateAuthRequest; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminInitiateAuthResponse; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminRespondToAuthChallengeRequest; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AdminRespondToAuthChallengeResponse; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AttributeType; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AuthFlowType; +import software.amazon.awssdk.services.cognitoidentityprovider.model.ChallengeNameType; +import software.amazon.awssdk.services.cognitoidentityprovider.model.DeliveryMediumType; + +import java.util.Map; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public abstract class CognitoSupport { + + private final String userPoolId = System.getenv("COGNITO_ID"); + private final String clientId = System.getenv("CLIENT_ID"); + private final CognitoIdentityProviderClient cognitoClient; + + protected CognitoSupport(CognitoIdentityProviderClient cognitoClient) { + this.cognitoClient = cognitoClient; + } + + protected AdminInitiateAuthResponse cognitoSignIn(String nickName, String password) { + Map authParams = Map.of( + "USERNAME", nickName, + "PASSWORD", password + ); + + return cognitoClient.adminInitiateAuth(AdminInitiateAuthRequest.builder() + .authFlow(AuthFlowType.ADMIN_NO_SRP_AUTH) + .authParameters(authParams) + .userPoolId(userPoolId) + .clientId(clientId) + .build()); + } + + protected AdminCreateUserResponse cognitoSignUp(SignUp signUp) { + + return cognitoClient.adminCreateUser(AdminCreateUserRequest.builder() + .userPoolId(userPoolId) + .username(signUp.nickName()) + .temporaryPassword(signUp.password()) + .userAttributes( + AttributeType.builder() + .name("given_name") + .value(signUp.firstName()) + .build(), + AttributeType.builder() + .name("family_name") + .value(signUp.lastName()) + .build(), + AttributeType.builder() + .name("email") + .value(signUp.email()) + .build(), + AttributeType.builder() + .name("email_verified") + .value("true") + .build()) + .desiredDeliveryMediums(DeliveryMediumType.EMAIL) + .messageAction("SUPPRESS") + .forceAliasCreation(Boolean.FALSE) + .build() + ); + } + + protected AdminRespondToAuthChallengeResponse confirmSignUp(SignUp signUp) { + AdminInitiateAuthResponse adminInitiateAuthResponse = cognitoSignIn(signUp.nickName(), signUp.password()); + + if (!ChallengeNameType.NEW_PASSWORD_REQUIRED.name().equals(adminInitiateAuthResponse.challengeNameAsString())) { + throw new RuntimeException("unexpected challenge: " + adminInitiateAuthResponse.challengeNameAsString()); + } + + Map challengeResponses = Map.of( + "USERNAME", signUp.nickName(), + "PASSWORD", signUp.password(), + "NEW_PASSWORD", signUp.password() + ); + + return cognitoClient.adminRespondToAuthChallenge(AdminRespondToAuthChallengeRequest.builder() + .challengeName(ChallengeNameType.NEW_PASSWORD_REQUIRED) + .challengeResponses(challengeResponses) + .userPoolId(userPoolId) + .clientId(clientId) + .session(adminInitiateAuthResponse.session()) + .build()); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetRootHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetRootHandler.java new file mode 100644 index 00000000..9c511102 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetRootHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.json.JSONObject; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public class GetRootHandler implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(new JSONObject().put("message", "Hello from api.").toString()); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetSecuredHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetSecuredHandler.java new file mode 100644 index 00000000..ea9ab183 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/GetSecuredHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.json.JSONObject; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public class GetSecuredHandler implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(new JSONObject().put("message", "Hello from secured.").toString()); + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignInHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignInHandler.java new file mode 100644 index 00000000..23bd335d --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignInHandler.java @@ -0,0 +1,55 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; + +import com.demoapigatewaycognito.dto.SignIn; +import org.json.JSONObject; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public class PostSignInHandler extends CognitoSupport implements RequestHandler { + + public PostSignInHandler(CognitoIdentityProviderClient cognitoClient) { + super(cognitoClient); + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + try { + SignIn signIn = SignIn.fromJson(requestEvent.getBody()); + + String accessToken = cognitoSignIn(signIn.nickName(), signIn.password()) + .authenticationResult() + .idToken(); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(new JSONObject().put("accessToken", accessToken).toString()); + } catch (Exception e) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(400) + .withBody(new JSONObject().put("error", e.getMessage()).toString()); + } + } + +} \ No newline at end of file diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignUpHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignUpHandler.java new file mode 100644 index 00000000..28a433a8 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/PostSignUpHandler.java @@ -0,0 +1,67 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import com.demoapigatewaycognito.dto.SignUp; +import org.json.JSONObject; +import software.amazon.awssdk.services.cognitoidentityprovider.CognitoIdentityProviderClient; +import software.amazon.awssdk.services.cognitoidentityprovider.model.AttributeType; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public class PostSignUpHandler extends CognitoSupport implements RequestHandler { + + public PostSignUpHandler(CognitoIdentityProviderClient cognitoClient) { + super(cognitoClient); + } + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + try { + SignUp signUp = SignUp.fromJson(requestEvent.getBody()); + + // sign up + String userId = cognitoSignUp(signUp) + .user().attributes().stream() + .filter(attr -> attr.name().equals("sub")) + .map(AttributeType::value) + .findAny() + .orElseThrow(() -> new RuntimeException("Sub not found.")); + // confirm sign up + String idToken = confirmSignUp(signUp) + .authenticationResult() + .idToken(); + + return new APIGatewayProxyResponseEvent() + .withStatusCode(200) + .withBody(new JSONObject() + .put("message", "User has been successfully signed up.") + .put("userId", userId) + .put("accessToken", idToken) + .toString()); + } catch (Exception e) { + return new APIGatewayProxyResponseEvent() + .withStatusCode(400) + .withBody(new JSONObject().put("error", e.getMessage()).toString()); + } + } + +} diff --git a/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/RouteNotImplementedHandler.java b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/RouteNotImplementedHandler.java new file mode 100644 index 00000000..1873ea2a --- /dev/null +++ b/examples/java/demo-apigateway-cognito/jsrc/main/java/com/demoapigatewaycognito/handler/RouteNotImplementedHandler.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024 EPAM Systems, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.demoapigatewaycognito.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent; +import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent; +import org.json.JSONObject; + +/** + * Created by Roman Ivanov on 7/20/2024. + */ +public class RouteNotImplementedHandler implements RequestHandler { + + @Override + public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent requestEvent, Context context) { + + return new APIGatewayProxyResponseEvent() + .withStatusCode(501) + .withBody( + new JSONObject().put( + "message", + "Handler for the %s method on the %s path is not implemented." + .formatted(requestEvent.getHttpMethod(), requestEvent.getPath()) + ).toString() + ); + } + +} diff --git a/examples/java/demo-apigateway-cognito/pom.xml b/examples/java/demo-apigateway-cognito/pom.xml new file mode 100644 index 00000000..9ce65be0 --- /dev/null +++ b/examples/java/demo-apigateway-cognito/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + demo-apigateway-cognito-group + demo-apigateway-cognito + 1.0.0 + + + 3.5.2 + 1.13.0 + 17 + 17 + UTF-8 + jsrc/main/java + jsrc/main/resources + + + + + + com.amazonaws + aws-lambda-java-core + 1.2.3 + + + com.amazonaws + aws-lambda-java-events + 3.12.0 + + + software.amazon.awssdk + cognitoidentityprovider + 2.26.21 + + + + org.json + json + 20240303 + + + + net.sf.aws-syndicate + deployment-configuration-annotations + ${syndicate.java.plugin.version} + + + + + ${src.dir} + + + ${resources.dir} + + + + + net.sf.aws-syndicate + deployment-configuration-maven-plugin + ${syndicate.java.plugin.version} + + + com.demoapigatewaycognito + + ${project.name}-${project.version}.jar + + + + generate-config + compile + false + + gen-deployment-config + assemble-lambda-layer-files + + + + + + org.apache.maven.plugins + maven-shade-plugin + ${maven-shade-plugin.version} + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + + false + + + + package + + shade + + + + + + + + diff --git a/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate.yml b/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate.yml new file mode 100644 index 00000000..d0948daa --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate.yml @@ -0,0 +1,13 @@ +account_id: ACCOUNT_ID +aws_access_key_id: ACCESS_KEY_ID +aws_secret_access_key: SECRET_ACCESS_KEY +deploy_target_bucket: YOUR_BUCKET_NAME +extended_prefix_mode: true +project_path: YOUR_PATH/demo-cognito-api-gateway +region: REGION +resources_prefix: demo- +resources_suffix: -dev +tags: + Env: dev + Stage: demo +use_temp_creds: false diff --git a/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate_aliases.yml b/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate_aliases.yml new file mode 100644 index 00000000..78f7a474 --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/.syndicate-config-lambda-basic/syndicate_aliases.yml @@ -0,0 +1,5 @@ +account_id: ACCOUNT_ID +lambdas_alias_name: dev +logs_expiration: 30 +region: REGION +userpool_name: USERPOOL_NAME diff --git a/examples/nodejs/demo-cognito-api-gateway/app/deployment_resources.json b/examples/nodejs/demo-cognito-api-gateway/app/deployment_resources.json new file mode 100644 index 00000000..1f5a70d7 --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/app/deployment_resources.json @@ -0,0 +1,91 @@ +{ + "LambdaBasicExecution": { + "policy_content": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "cognito-idp:GetUser", + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:GetIdentityProviderByIdentifier", + "cognito-idp:ListUserPoolClients", + "cognito-idp:AdminRespondToAuthChallenge" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "resource_type": "iam_policy" + }, + "BasicExecutionRole": { + "predefined_policies": [], + "principal_service": "lambda", + "custom_policies": [ + "LambdaBasicExecution" + ], + "resource_type": "iam_role", + "allowed_accounts": [ + "${account_id}" + ] + }, + "${userpool_name}": { + "resource_type": "cognito_idp", + "password_policy": { + "require_uppercase": true, + "require_numbers": true + }, + "auto_verified_attributes": [], + "sms_configuration": {}, + "username_attributes": [], + "client": { + "client_name": "client-app", + "generate_secret": false, + "explicit_auth_flows": [ + "ALLOW_ADMIN_USER_PASSWORD_AUTH", + "ALLOW_CUSTOM_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + } + }, + "syndicate-demo-api": { + "authorizers": { + "authorizer": { + "type": "COGNITO_USER_POOLS", + "identity_source": "method.request.header.Authorization", + "user_pools": [ + "${userpool_name}" + ], + "ttl": 300 + } + }, + "deploy_stage": "dev", + "dependencies": [ + { + "resource_name": "demo-cognito-project", + "resource_type": "lambda" + } + ], + "resources": { + "/login": { + "enable_cors": true, + "POST": { + "enable_proxy": true, + "integration_request_body_template": {}, + "authorization_type": "NONE", + "integration_type": "lambda", + "method_request_parameters": {}, + "default_error_pattern": true, + "integration_passthrough_behavior": "WHEN_NO_TEMPLATES", + "lambda_name": "demo-cognito-project" + } + } + }, + "resource_type": "api_gateway" + } +} \ No newline at end of file diff --git a/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/index.js b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/index.js new file mode 100644 index 00000000..b85b1f5c --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/index.js @@ -0,0 +1,29 @@ +const AWS = require('aws-sdk'); +const CognitoIdentityServiceProvider = AWS.CognitoIdentityServiceProvider; + +const userPoolId = process.env.CUPId; +const clientId = process.env.CUPClientId; + +const cognitoIdentityServiceProvider = new CognitoIdentityServiceProvider(); + +exports.handler = async (event) => { + console.log(event); + const body = JSON.parse(event.body); + const params = { + AuthFlow: 'ADMIN_NO_SRP_AUTH', + ClientId: clientId, + AuthParameters: { + USERNAME: body.email, + PASSWORD: body.password + } + }; + + try { + const data = await cognitoIdentityServiceProvider.initiateAuth(params).promise(); + const idToken = data.AuthenticationResult.IdToken; + return idToken; + } catch (error) { + console.error(error); + throw error; + } +}; \ No newline at end of file diff --git a/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/lambda_config.json b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/lambda_config.json new file mode 100644 index 00000000..25dc7923 --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/lambda_config.json @@ -0,0 +1,34 @@ +{ + "version": "1.0", + "name": "demo-cognito-project", + "func_name": "lambdas/demo-cognito-project/index.handler", + "resource_type": "lambda", + "iam_role_name": "BasicExecutionRole", + "runtime": "nodejs20.x", + "memory": 128, + "timeout": 100, + "lambda_path": "lambdas\\demo-cognito-project", + "dependencies": [ + { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp" + } + ], + "event_sources": [], + "env_variables": { + "CUPId": { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp", + "parameter": "id" + }, + "CUPClientId": { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp", + "parameter": "client_id" + } + }, + "publish_version": true, + "alias": "${lambdas_alias_name}", + "url_config": {}, + "ephemeral_storage": 512 +} \ No newline at end of file diff --git a/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package-lock.json b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package-lock.json new file mode 100644 index 00000000..d804c57d --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package-lock.json @@ -0,0 +1,444 @@ +{ + "name": "demo-cognito-project", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "demo-cognito-project", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "aws-sdk": "^2.1011.0", + "uuid": "^9.0.1" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-sdk": { + "version": "2.1659.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1659.0.tgz", + "integrity": "sha512-WOoy5DdWW4kpQuxjWiQdoSDR+dT/HeAUwjb6b+8taEMZzvUzp3fmdDwdryUTlLWGxrnb7ru2yu5pryjhPOzANg==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-sdk/node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package.json b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package.json new file mode 100644 index 00000000..facdc245 --- /dev/null +++ b/examples/nodejs/demo-cognito-api-gateway/app/lambdas/demo-cognito-project/package.json @@ -0,0 +1,13 @@ +{ + "name": "demo-cognito-project", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": {}, + "author": "", + "license": "ISC", + "dependencies": { + "uuid": "^9.0.1", + "aws-sdk": "^2.1011.0" + } +} \ No newline at end of file diff --git a/examples/nodejs/demo/app/lambdas/demo-basic-project/package.json b/examples/nodejs/demo/app/lambdas/demo-basic-project/package.json index 5c7b78e3..3af3739c 100644 --- a/examples/nodejs/demo/app/lambdas/demo-basic-project/package.json +++ b/examples/nodejs/demo/app/lambdas/demo-basic-project/package.json @@ -7,7 +7,7 @@ "author": "", "license": "ISC", "dependencies": { - "uuid": "^9.0.8", + "uuid": "^9.0.1", "aws-sdk": "^2.1011.0" } } \ No newline at end of file diff --git a/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate.yml b/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate.yml new file mode 100644 index 00000000..b8e73e12 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate.yml @@ -0,0 +1,13 @@ +account_id: ACCOUNT_ID +aws_access_key_id: ACCESS_KEY_ID +aws_secret_access_key: SECRET_ACCESS_KEY +deploy_target_bucket: YOUR_BUCKET_NAME +extended_prefix_mode: true +project_path: YOUR_PATH/lambda-cognito-api-gateway +region: REGION +resources_prefix: demo- +resources_suffix: -dev +tags: + Env: dev + Stage: demo +use_temp_creds: false diff --git a/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate_aliases.yml b/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate_aliases.yml new file mode 100644 index 00000000..78f7a474 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/.syndicate-config-lambda-dynamo-api-gateway/syndicate_aliases.yml @@ -0,0 +1,5 @@ +account_id: ACCOUNT_ID +lambdas_alias_name: dev +logs_expiration: 30 +region: REGION +userpool_name: USERPOOL_NAME diff --git a/examples/python/lambda-cognito-api-gateway/CHANGELOG.md b/examples/python/lambda-cognito-api-gateway/CHANGELOG.md new file mode 100644 index 00000000..9ea15037 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/CHANGELOG.md @@ -0,0 +1,15 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - yyyy-MM-dd +### Added + - Added items + +### Changed + - Changed items + +### Removed + - Removed items diff --git a/examples/python/lambda-cognito-api-gateway/README.md b/examples/python/lambda-cognito-api-gateway/README.md new file mode 100644 index 00000000..80750f21 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/README.md @@ -0,0 +1,76 @@ +#### This example shows a Syndicate configuration for deploying: +* 1 Lambda function; +* 1 IAM role attached to lambda; +* 1 Custom IAM policy attached to role; +* 1 API Gateway; +* 1 Cognito User Pool + +#### To deploy this example: + +##### 1. Replace following placeholders in `syndicate.yml`: +* `YOUR_PATH` - actual path of the project; +* `YOUR_BUCKET_NAME` - name of AWS S3 bucket where you want syndicate to store projects artifacts; +* `ACCOUNT_ID` - AWS account id where syndicate will deploy this demo; +* `ACCESS_KEY_ID` - your Secret access key acceptable of account specified in account_id; +* `SECRET_ACCESS_KEY` - your Access key ID acceptable of account specified in account_id; +* `REGION` - AWS region where syndicate will deploy this demo; + +##### 2. Replace following placeholder in `syndicate_aliases.yml`: +* `ACCOUNT_ID` - AWS account id where syndicate will deploy this demo; +* `REGION` - AWS region where syndicate will deploy this demo; +* `USERPOOL_NAME` - name for the user pool name to deploy; + +##### 3. Export path to config files: +`export SDCT_CONF=$YOUR_PATH/.syndicate-config-lambda-dynamo-api-gateway` + +##### 4. Build bundle: +`syndicate build` + +##### 5. Deploy: +`syndicate deploy` + +##### 6. Check api was created: +`aws apigateway get-rest-apis` + +Response must contain just created `syndicate-demo-api`: + +```json +{ + "items": [ + { + "id": "bzztcmtw94", + "name": "syndicate-demo-api", + "createdDate": "2021-03-11T10:59:33+02:00", + "apiKeySource": "HEADER", + "endpointConfiguration": { + "types": [ + "EDGE" + ] + }, + "disableExecuteApiEndpoint": false + } + ] +} +``` + +##### 7. Trigger deployed lambda using aws-cli: + +`aws lambda invoke --function-name lambda_example --payload '{"username": "example@gmail.com", "password": "some text"}' --cli-binary-format raw-in-base64-out response.json` + +Response content will be stored in `response.json`: + +```json +{ + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": "{...}" +} +``` + +#### To clean project resources + +##### 1. Clean: +`syndicate clean` + diff --git a/examples/python/lambda-cognito-api-gateway/__init__.py b/examples/python/lambda-cognito-api-gateway/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/python/lambda-cognito-api-gateway/deployment_resources.json b/examples/python/lambda-cognito-api-gateway/deployment_resources.json new file mode 100644 index 00000000..db3ee4ae --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/deployment_resources.json @@ -0,0 +1,91 @@ +{ + "LambdaBasicExecution": { + "policy_content": { + "Statement": [ + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents", + "cognito-idp:GetUser", + "cognito-idp:AdminCreateUser", + "cognito-idp:AdminInitiateAuth", + "cognito-idp:GetIdentityProviderByIdentifier", + "cognito-idp:ListUserPoolClients", + "cognito-idp:AdminRespondToAuthChallenge" + ], + "Effect": "Allow", + "Resource": "*" + } + ], + "Version": "2012-10-17" + }, + "resource_type": "iam_policy" + }, + "BasicExecutionRole": { + "predefined_policies": [], + "principal_service": "lambda", + "custom_policies": [ + "LambdaBasicExecution" + ], + "resource_type": "iam_role", + "allowed_accounts": [ + "${account_id}" + ] + }, + "${userpool_name}": { + "resource_type": "cognito_idp", + "password_policy": { + "require_uppercase": true, + "require_numbers": true + }, + "auto_verified_attributes": [], + "sms_configuration": {}, + "username_attributes": [], + "client": { + "client_name": "client-app", + "generate_secret": false, + "explicit_auth_flows": [ + "ALLOW_ADMIN_USER_PASSWORD_AUTH", + "ALLOW_CUSTOM_AUTH", + "ALLOW_USER_SRP_AUTH", + "ALLOW_REFRESH_TOKEN_AUTH" + ] + } + }, + "syndicate-demo-api": { + "authorizers": { + "authorizer": { + "type": "COGNITO_USER_POOLS", + "identity_source": "method.request.header.Authorization", + "user_pools": [ + "${userpool_name}" + ], + "ttl": 300 + } + }, + "deploy_stage": "dev", + "dependencies": [ + { + "resource_name": "lambda_example", + "resource_type": "lambda" + } + ], + "resources": { + "/login": { + "enable_cors": true, + "POST": { + "enable_proxy": true, + "integration_request_body_template": {}, + "authorization_type": "NONE", + "integration_type": "lambda", + "method_request_parameters": {}, + "default_error_pattern": true, + "integration_passthrough_behavior": "WHEN_NO_TEMPLATES", + "lambda_name": "lambda_example" + } + } + }, + "resource_type": "api_gateway" + } +} \ No newline at end of file diff --git a/examples/python/lambda-cognito-api-gateway/src/__init__.py b/examples/python/lambda-cognito-api-gateway/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/python/lambda-cognito-api-gateway/src/lambdas/__init__.py b/examples/python/lambda-cognito-api-gateway/src/lambdas/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/__init__.py b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/handler.py b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/handler.py new file mode 100644 index 00000000..13a51460 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/handler.py @@ -0,0 +1,54 @@ +""" + Copyright 2018 EPAM Systems, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" +import json +import os + +import boto3 + + +def lambda_handler(event, context): + print(event) + body = json.loads(event['body']) + email = body.get('email') + password = body.get('password') + + auth_result = admin_initiate_auth(username=email, password=password) + if auth_result: + id_token = auth_result['AuthenticationResult']['IdToken'] + else: + id_token = None + + return { + "statusCode": 200, + "headers": { + "Content-Type": "application/json" + }, + "body": json.dumps(id_token) + } + + +def admin_initiate_auth(username, password): + auth_params = { + 'USERNAME': username, + 'PASSWORD': password + } + cognito_client = boto3.client('cognito-idp', + os.environ.get('region', 'eu-central-1')) + result = cognito_client.admin_initiate_auth( + UserPoolId=os.environ.get('cup_id'), + ClientId=os.environ.get('cup_client_id'), + AuthFlow='ADMIN_NO_SRP_AUTH', AuthParameters=auth_params) + return result diff --git a/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/lambda_config.json b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/lambda_config.json new file mode 100644 index 00000000..b6370e23 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/lambda_config.json @@ -0,0 +1,30 @@ +{ + "version": "1.0", + "name": "lambda_example", + "func_name": "handler.lambda_handler", + "resource_type": "lambda", + "iam_role_name": "BasicExecutionRole", + "runtime": "python3.10", + "memory": 128, + "timeout": 300, + "lambda_path": "", + "dependencies": [ + { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp" + } + ], + "env_variables": { + "cup_id": { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp", + "parameter": "id" + }, + "cup_client_id": { + "resource_name": "${userpool_name}", + "resource_type": "cognito_idp", + "parameter": "client_id" + } + }, + "region": "${region}" +} \ No newline at end of file diff --git a/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/requirements.txt b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/requirements.txt new file mode 100644 index 00000000..1db657b6 --- /dev/null +++ b/examples/python/lambda-cognito-api-gateway/src/lambdas/lambda_example/requirements.txt @@ -0,0 +1 @@ +boto3 \ No newline at end of file diff --git a/plugin/deployment-configuration-annotations/pom.xml b/plugin/deployment-configuration-annotations/pom.xml index 1b13d27d..908e0ff4 100644 --- a/plugin/deployment-configuration-annotations/pom.xml +++ b/plugin/deployment-configuration-annotations/pom.xml @@ -7,11 +7,11 @@ net.sf.aws-syndicate deployment-configuration-processor - 1.11.1 + 1.13.0 deployment-configuration-annotations - 1.11.1 + 1.13.0 jar diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/environment/EnvironmentVariable.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/environment/EnvironmentVariable.java index 65500eba..006dc4ad 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/environment/EnvironmentVariable.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/environment/EnvironmentVariable.java @@ -16,6 +16,8 @@ package com.syndicate.deployment.annotations.environment; +import com.syndicate.deployment.model.environment.ValueTransformer; + import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; @@ -34,4 +36,6 @@ String value(); + ValueTransformer valueTransformer() default ValueTransformer.NONE; + } diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/FunctionResponseType.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/FunctionResponseType.java new file mode 100644 index 00000000..888683aa --- /dev/null +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/FunctionResponseType.java @@ -0,0 +1,19 @@ +package com.syndicate.deployment.annotations.events; + +import com.fasterxml.jackson.annotation.JsonValue; + +public enum FunctionResponseType { + + REPORT_BATCH_ITEM_FAILURES("ReportBatchItemFailures"); + + private final String jsonValue; + + FunctionResponseType(String jsonValue) { + this.jsonValue = jsonValue; + } + + @JsonValue + public String getJsonValue() { + return jsonValue; + } +} diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/SqsTriggerEventSource.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/SqsTriggerEventSource.java index 4b710071..68e77096 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/SqsTriggerEventSource.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/annotations/events/SqsTriggerEventSource.java @@ -38,4 +38,5 @@ int batchSize(); + FunctionResponseType[] functionResponseTypes() default {}; } diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/EventSourceType.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/EventSourceType.java index f448beda..8003e442 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/EventSourceType.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/EventSourceType.java @@ -122,7 +122,9 @@ public EventSourceItem createEventSourceItem(Annotation eventSource) { SqsTriggerEventSource sqsEventSource = (SqsTriggerEventSource) eventSource; return new SqsTriggerEventSourceItem.Builder() .withTargetQueue(sqsEventSource.targetQueue()) - .withBatchSize(sqsEventSource.batchSize()).build(); + .withBatchSize(sqsEventSource.batchSize()) + .withFunctionResponseTypes(sqsEventSource.functionResponseTypes()) + .build(); } @Override diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/LambdaConfiguration.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/LambdaConfiguration.java index c7c11289..588fa1ab 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/LambdaConfiguration.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/LambdaConfiguration.java @@ -89,7 +89,7 @@ public class LambdaConfiguration { private Set eventSources; @JsonProperty("env_variables") - private Map variables; + private Map variables; @JsonProperty("dl_resource_name") private String dlResourceName; @@ -193,7 +193,7 @@ public Set getEventSources() { return eventSources; } - public Map getVariables() { + public Map getVariables() { return variables; } @@ -382,7 +382,7 @@ public Builder withEventSources(Set events) { return this; } - public Builder withVariables(Map variables) { + public Builder withVariables(Map variables) { Objects.requireNonNull(variables, "Variables cannot be null"); configuration.variables = variables; return this; diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/ResourceType.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/ResourceType.java index 93e8098e..1abd059f 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/ResourceType.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/ResourceType.java @@ -23,6 +23,7 @@ */ public enum ResourceType { + COGNITO_USER_POOL("cognito_idp"), CLOUDWATCH_RULE("cloudwatch_rule"), EVENTBRIDGE_RULE("eventbridge_rule"), LAMBDA("lambda"), diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/environment/ValueTransformer.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/environment/ValueTransformer.java new file mode 100644 index 00000000..fcbbb6fd --- /dev/null +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/environment/ValueTransformer.java @@ -0,0 +1,30 @@ +package com.syndicate.deployment.model.environment; + +public enum ValueTransformer { + + NONE(null, null, null), + USER_POOL_NAME_TO_USER_POOL_ID( "resource_name", "cognito_idp", "id"), + USER_POOL_NAME_TO_CLIENT_ID( "resource_name", "cognito_idp", "client_id"); + + private final String sourceParameter; + private final String resourceType; + private final String parameter; + + ValueTransformer(String sourceParameter, String resourceType, String targetParameter) { + this.sourceParameter = sourceParameter; + this.resourceType = resourceType; + this.parameter = targetParameter; + } + + public String getSourceParameter() { + return sourceParameter; + } + + public String getResourceType() { + return resourceType; + } + + public String getParameter() { + return parameter; + } +} diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/events/SqsTriggerEventSourceItem.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/events/SqsTriggerEventSourceItem.java index 0bc0ce35..a6b808c7 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/events/SqsTriggerEventSourceItem.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/events/SqsTriggerEventSourceItem.java @@ -17,8 +17,10 @@ package com.syndicate.deployment.model.events; import com.fasterxml.jackson.annotation.JsonProperty; +import com.syndicate.deployment.annotations.events.FunctionResponseType; import com.syndicate.deployment.model.EventSourceType; +import java.util.Arrays; import java.util.Objects; /** @@ -32,6 +34,9 @@ public class SqsTriggerEventSourceItem extends EventSourceItem { @JsonProperty("batch_size") private int batchSize; + @JsonProperty("function_response_types") + private FunctionResponseType[] functionResponseTypes; + private SqsTriggerEventSourceItem() { } @@ -43,6 +48,10 @@ public int getBatchSize() { return batchSize; } + public FunctionResponseType[] getFunctionResponseTypes() { + return functionResponseTypes; + } + public static class Builder { private final SqsTriggerEventSourceItem triggerEventSourceItem = new SqsTriggerEventSourceItem(); @@ -63,6 +72,11 @@ public SqsTriggerEventSourceItem build() { triggerEventSourceItem.eventSourceType = EventSourceType.SQS_TRIGGER; return triggerEventSourceItem; } + + public Builder withFunctionResponseTypes(FunctionResponseType[] functionResponseTypes) { + this.triggerEventSourceItem.functionResponseTypes = functionResponseTypes; + return this; + } } @Override @@ -72,7 +86,7 @@ public boolean equals(Object o) { SqsTriggerEventSourceItem that = (SqsTriggerEventSourceItem) o; - return batchSize == that.batchSize && eventSourceType == that.eventSourceType && targetQueue.equals(that.targetQueue); + return batchSize == that.batchSize && eventSourceType == that.eventSourceType && targetQueue.equals(that.targetQueue) && functionResponseTypes == that.functionResponseTypes; } @@ -81,6 +95,7 @@ public int hashCode() { int result = targetQueue.hashCode(); result = 31 * result + eventSourceType.hashCode(); result = 31 * result + batchSize; + result = 31 * result + Arrays.hashCode(functionResponseTypes); return result; } @@ -89,7 +104,7 @@ public String toString() { return "SqsTriggerEventSourceItem{" + "targetQueue='" + targetQueue + '\'' + ", batchSize=" + batchSize + - "} " + super.toString(); + ", functionResponseTypes=" + Arrays.toString(functionResponseTypes) + + '}'; } - } diff --git a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/terraform/TerraformLambdaConfiguration.java b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/terraform/TerraformLambdaConfiguration.java index 15a78104..8ea18254 100644 --- a/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/terraform/TerraformLambdaConfiguration.java +++ b/plugin/deployment-configuration-annotations/src/main/java/com/syndicate/deployment/model/terraform/TerraformLambdaConfiguration.java @@ -70,7 +70,7 @@ public class TerraformLambdaConfiguration { private TerraformLambdaVpcConfig vpcConfig; @JsonProperty("environment") - private Map environmentVariables; + private Map environmentVariables; @JsonProperty("dead_letter_config") private Map deadLetterConfig; @@ -132,7 +132,7 @@ public Builder withVpcConfig(TerraformLambdaVpcConfig config) { return this; } - public Builder withEnvironmentVariables(Map environmentVariables) { + public Builder withEnvironmentVariables(Map environmentVariables) { Objects.requireNonNull(environmentVariables, "Environment variables cannot be null"); if (environmentVariables.isEmpty()) { throw new InvalidParameterException("Environment variables cannot be empty"); @@ -197,7 +197,7 @@ public TerraformLambdaVpcConfig getVpcConfig() { return vpcConfig; } - public Map getEnvironmentVariables() { + public Map getEnvironmentVariables() { return environmentVariables; } } diff --git a/plugin/deployment-configuration-maven-plugin/pom.xml b/plugin/deployment-configuration-maven-plugin/pom.xml index 2796fabe..1b7ddb7e 100644 --- a/plugin/deployment-configuration-maven-plugin/pom.xml +++ b/plugin/deployment-configuration-maven-plugin/pom.xml @@ -7,11 +7,11 @@ net.sf.aws-syndicate deployment-configuration-processor - 1.11.1 + 1.13.0 deployment-configuration-maven-plugin - 1.11.1 + 1.13.0 maven-plugin @@ -28,7 +28,7 @@ net.sf.aws-syndicate deployment-configuration-annotations - 1.11.1 + 1.13.0 org.reflections diff --git a/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/factories/LambdaConfigurationFactory.java b/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/factories/LambdaConfigurationFactory.java index 1d33b835..fcc67f9a 100644 --- a/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/factories/LambdaConfigurationFactory.java +++ b/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/factories/LambdaConfigurationFactory.java @@ -45,7 +45,7 @@ private LambdaConfigurationFactory() { public static LambdaConfiguration createLambdaConfiguration(String version, Class lambdaClass, LambdaHandler lambdaHandler, Set dependencies, - Set events, Map variables, + Set events, Map variables, String packageName, String lambdaPath) { StringBuilder function = new StringBuilder(lambdaClass.getName()); String methodName = lambdaHandler.methodName(); diff --git a/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/processor/impl/LambdaHandlerAnnotationProcessor.java b/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/processor/impl/LambdaHandlerAnnotationProcessor.java index 76b53885..faf95a45 100644 --- a/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/processor/impl/LambdaHandlerAnnotationProcessor.java +++ b/plugin/deployment-configuration-maven-plugin/src/main/java/com/syndicate/deployment/processor/impl/LambdaHandlerAnnotationProcessor.java @@ -17,6 +17,7 @@ package com.syndicate.deployment.processor.impl; import com.syndicate.deployment.annotations.environment.EnvironmentVariable; +import com.syndicate.deployment.model.environment.ValueTransformer; import com.syndicate.deployment.annotations.events.DynamoDbTriggerEventSource; import com.syndicate.deployment.annotations.events.EventBridgeRuleSource; import com.syndicate.deployment.annotations.events.RuleEventSource; @@ -79,7 +80,7 @@ protected Pair process(Class lambdaClass, String for (DependsOn annotation : dependsOnAnnotations) { dependencies.add(DependencyItemFactory.createDependencyItem(annotation)); } - Map envVariables = getEnvVariables(lambdaClass); + Map envVariables = getEnvVariables(lambdaClass); LambdaConfiguration lambdaConfiguration = LambdaConfigurationFactory.createLambdaConfiguration(version, lambdaClass, lambdaHandler, dependencies, events, envVariables, fileName, path); return new Pair<>(lambdaHandler.lambdaName(), lambdaConfiguration); @@ -94,12 +95,20 @@ public List> getAnnotatedClasses(String[] packages) { return lambdasClasses; } - private Map getEnvVariables(Class lambdaClass) { - Map envVariables = new HashMap<>(); + private Map getEnvVariables(Class lambdaClass) { + Map envVariables = new HashMap<>(); EnvironmentVariable[] environmentVariablesAnnotations = lambdaClass.getDeclaredAnnotationsByType(EnvironmentVariable.class); for (EnvironmentVariable annotation : environmentVariablesAnnotations) { - envVariables.put(annotation.key(), annotation.value()); + envVariables.put(annotation.key(), getValue(annotation)); } return envVariables; } + + private Object getValue(EnvironmentVariable annotation) { + return annotation.valueTransformer() == ValueTransformer.NONE + ? annotation.value() + : Map.of(annotation.valueTransformer().getSourceParameter(), annotation.value(), + "resource_type", annotation.valueTransformer().getResourceType(), + "parameter", annotation.valueTransformer().getParameter()); + } } diff --git a/plugin/pom.xml b/plugin/pom.xml index e96bc90b..a1ff802c 100644 --- a/plugin/pom.xml +++ b/plugin/pom.xml @@ -6,7 +6,7 @@ net.sf.aws-syndicate deployment-configuration-processor - 1.11.1 + 1.13.0 pom aws-syndicate @@ -53,7 +53,7 @@ 2.16.1 3.8.0 2.8.2 - 1.6 + 3.2.4 3.0.1 3.0.1 1.6.8 diff --git a/setup.py b/setup.py index 7ba3c58b..0653310c 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ setup( name='aws-syndicate', - version='1.12.0', + version='1.13.0', packages=find_packages(), include_package_data=True, install_requires=[ diff --git a/syndicate/connection/__init__.py b/syndicate/connection/__init__.py index b98e4b07..9ee8c56a 100644 --- a/syndicate/connection/__init__.py +++ b/syndicate/connection/__init__.py @@ -15,6 +15,8 @@ """ from functools import lru_cache +from botocore.config import Config + from syndicate.connection.api_gateway_connection import ApiGatewayConnection, \ ApiGatewayV2Connection from syndicate.connection.application_autoscaling_connection import ( @@ -51,6 +53,12 @@ class ConnectionProvider(object): def __init__(self, credentials): self.credentials = credentials.copy() + self.client_config = Config( + retries={ + 'max_attempts': 10, + 'mode': 'standard' + } + ) @lru_cache(maxsize=None) def api_gateway(self, region=None): @@ -95,10 +103,11 @@ def cognito_identity(self, region=None): @lru_cache(maxsize=None) def cognito_identity_provider(self, region=None): - credentials = self.credentials.copy() + params = self.credentials.copy() if region: - credentials['region'] = region - return CognitoIdentityProviderConnection(**credentials) + params['region'] = region + params['client_config'] = self.client_config + return CognitoIdentityProviderConnection(**params) @lru_cache(maxsize=None) def iam(self): diff --git a/syndicate/connection/api_gateway_connection.py b/syndicate/connection/api_gateway_connection.py index 35eb3084..56a0d5d9 100644 --- a/syndicate/connection/api_gateway_connection.py +++ b/syndicate/connection/api_gateway_connection.py @@ -121,10 +121,17 @@ def get_api_by_name(self, api_name): :type api_name: str """ apis = self.get_all_apis() - if apis: - for each in apis: - if each['name'] == api_name: - return each + target_apis = [api for api in apis if api['name'] == api_name] + if len(target_apis) == 1: + return target_apis[0] + if len(target_apis) > 1: + _LOG.warn(f"API Gateway can\'t be identified unambiguously " + f"because there is more than one resource with the name " + f"'{api_name}' in the region {self.region}. Determined " + f"APIs: {[api['name'] for api in target_apis]}") + else: + _LOG.warn(f'API Gateway with the name "{api_name}" ' + f'not found in the region {self.region}') def get_api_id(self, api_name): """ diff --git a/syndicate/connection/cloud_watch_connection.py b/syndicate/connection/cloud_watch_connection.py index e8729888..94917b49 100644 --- a/syndicate/connection/cloud_watch_connection.py +++ b/syndicate/connection/cloud_watch_connection.py @@ -295,6 +295,11 @@ def list_targets(self, rule_name): """ return self.client.list_targets_by_rule(Rule=rule_name) + def list_rules_by_target(self, target_arn): + response = self.client.list_rule_names_by_target(TargetArn=target_arn) + rule_names = response.get('RuleNames', []) + return rule_names + def list_rules(self): """ Get list of rules for region.""" rules = [] diff --git a/syndicate/connection/cognito_identity_provider_connection.py b/syndicate/connection/cognito_identity_provider_connection.py index 1477a644..0dd17847 100644 --- a/syndicate/connection/cognito_identity_provider_connection.py +++ b/syndicate/connection/cognito_identity_provider_connection.py @@ -25,7 +25,7 @@ class CognitoIdentityProviderConnection(object): """ Cognito identity provider connection class.""" - def __init__(self, region=None, aws_access_key_id=None, + def __init__(self, client_config, region=None, aws_access_key_id=None, aws_secret_access_key=None, aws_session_token=None): self.region = region self.aws_access_key_id = aws_access_key_id @@ -34,7 +34,8 @@ def __init__(self, region=None, aws_access_key_id=None, self.client = client('cognito-idp', region, aws_access_key_id=aws_access_key_id, aws_secret_access_key=aws_secret_access_key, - aws_session_token=aws_session_token) + aws_session_token=aws_session_token, + config=client_config) _LOG.debug('Opened new Cognito identity connection.') def create_user_pool(self, pool_name, auto_verified_attributes=None, @@ -104,11 +105,78 @@ def create_user_pool_client( response = self.client.create_user_pool_client(**params) return response['UserPoolClient'].get('ClientId') + def list_user_pool_clients(self, cup_id): + + response = self.client.list_user_pool_clients( + UserPoolId=cup_id, + MaxResults=5 + ) + + client_ids = [cup_client['ClientId'] for + cup_client in response['UserPoolClients']] + next_token = response.get('NextToken') + + while next_token: + response = self.client.list_user_pool_clients( + UserPoolId=cup_id, + MaxResults=5, + NextToken=next_token + ) + client_ids.extend([cup_client['ClientId'] for + cup_client in response['UserPoolClients']]) + next_token = response.get('NextToken') + + return client_ids + + def if_cup_client_exist(self, cup_name): + + cup_id = self.if_pool_exists_by_name(cup_name) + if not cup_id: + return + + client_ids = self.list_user_pool_clients(cup_id) + + if len(client_ids) == 1: + return client_ids[0] + if len(client_ids) > 1: + _LOG.warn(f'Client ID of Cognito User Pool "{cup_name}" can\'t be ' + f'identified unambiguously because there is more than ' + f'one client in the Cognito User Pool. Determined IDs: ' + f'"{client_ids}"') + else: + _LOG.warn(f'Clients not found in the Cognito User Pool with the ' + f'name "{cup_name}"') + def if_pool_exists_by_name(self, user_pool_name): ids = [] - for pool in self.client.list_user_pools(MaxResults=60)['UserPools']: - if pool.get('Name') == user_pool_name: - ids.append(pool['Id']) + paginator = self.client.get_paginator('list_user_pools') + response = paginator.paginate( + PaginationConfig={ + 'MaxItems': 60, + 'PageSize': 10 + } + ) + for page in response: + ids.extend( + [user_pool['Id'] for user_pool in page['UserPools'] if + user_pool['Name'] == user_pool_name] + ) + next_token = response.resume_token + while next_token: + response = paginator.paginate( + PaginationConfig={ + 'MaxItems': 60, + 'PageSize': 10, + 'StartingToken': next_token + } + ) + for page in response: + ids.extend( + [user_pool['Id'] for user_pool in page['UserPools'] if + user_pool['Name'] == user_pool_name] + ) + next_token = response.resume_token + if len(ids) == 1: return ids[0] if len(ids) > 1: diff --git a/syndicate/connection/helper.py b/syndicate/connection/helper.py index e5d75cc8..4843a3dd 100644 --- a/syndicate/connection/helper.py +++ b/syndicate/connection/helper.py @@ -86,6 +86,7 @@ def wrapper(*args, **kwargs): 'UpdateGatewayResponse', 'Cannot delete, found existing JobQueue relationship', 'Cannot delete, resource is being modified', + 'Please try again' ] last_ex = None for each in range(1, retry_timeout, retry_timeout_step): diff --git a/syndicate/connection/lambda_connection.py b/syndicate/connection/lambda_connection.py index d3a8f990..b79e0bed 100644 --- a/syndicate/connection/lambda_connection.py +++ b/syndicate/connection/lambda_connection.py @@ -288,7 +288,8 @@ def get_alias(self, function_name, name): def add_event_source(self, func_name, stream_arn, batch_size=10, batch_window: Optional[int] = None, start_position=None, - filters: Optional[List] = None): + filters: Optional[List] = None, + function_response_types: Optional[List] = None): """ Create event source for Lambda :type func_name: str :type stream_arn: str @@ -296,6 +297,8 @@ def add_event_source(self, func_name, stream_arn, batch_size=10, :param batch_size: max limit of Lambda event process in one time :param start_position: option for Lambda reading event mode :param filters: Optional[list] + :param function_response_types: Optional[list] list of function + response types :return: response """ params = dict( @@ -308,7 +311,8 @@ def add_event_source(self, func_name, stream_arn, batch_size=10, params['StartingPosition'] = start_position if filters: params['FilterCriteria'] = {'Filters': filters} - + if function_response_types: + params['FunctionResponseTypes'] = function_response_types response = self.client.create_event_source_mapping(**params) return response @@ -498,7 +502,8 @@ def update_code_source(self, lambda_name, s3_bucket, s3_key, Publish=publish_version) def update_event_source(self, uuid, function_name, batch_size, - batch_window=None, filters: Optional[List] = None): + batch_window=None, filters: Optional[List] = None, + function_response_types: Optional[List] = None): params = dict( UUID=uuid, FunctionName=function_name, BatchSize=batch_size ) @@ -506,6 +511,10 @@ def update_event_source(self, uuid, function_name, batch_size, params['MaximumBatchingWindowInSeconds'] = batch_window if filters is not None: params['FilterCriteria'] = {'Filters': filters} + if function_response_types: + params['FunctionResponseTypes'] = function_response_types + else: + params['FunctionResponseTypes'] = [] return self.client.update_event_source_mapping(**params) def get_function(self, lambda_name, qualifier=None): @@ -584,7 +593,7 @@ def update_lambda_configuration(self, lambda_name, role=None, handler=None, env_vars=None, runtime=None, dead_letter_arn=None, kms_key_arn=None, layers=None, ephemeral_storage=None, - snap_start: str =None): + snap_start: str = None): params = dict(FunctionName=lambda_name) if ephemeral_storage: params['EphemeralStorage'] = {'Size': ephemeral_storage} diff --git a/syndicate/connection/s3_connection.py b/syndicate/connection/s3_connection.py index 060daca6..724e84fe 100644 --- a/syndicate/connection/s3_connection.py +++ b/syndicate/connection/s3_connection.py @@ -22,6 +22,7 @@ from syndicate.commons.log_helper import get_logger from syndicate.connection.helper import apply_methods_decorator, retry +from syndicate.core.decorators import threading_lock _LOG = get_logger('syndicate.connection.s3_connection') @@ -182,9 +183,13 @@ def remove_bucket(self, bucket_name): def delete_bucket(self, bucket_name): self.client.delete_bucket(Bucket=bucket_name) + @threading_lock def configure_event_source_for_lambda(self, bucket, lambda_arn, event_sources): - """ + """ Create event notification in the bucket that triggers the lambda + Note: two identical events can't be configured for two + separate lambdas in one bucket + :type bucket: str :type lambda_arn: str :type event_sources: list[dict] @@ -202,7 +207,22 @@ def configure_event_source_for_lambda(self, bucket, lambda_arn, - filter_rules (optional): list[dict] - list of S3 event filters: {'Name': 'prefix'|'suffix', 'Value': 'string'} """ - config = {'LambdaFunctionConfigurations': []} + config = self.get_bucket_notification(bucket_name=bucket) + + config.pop('ResponseMetadata') + + if 'LambdaFunctionConfigurations' not in config: + config['LambdaFunctionConfigurations'] = [] + + # for some reason filter rule's name value is in uppercase when + # should be in lower according to the documentation + for lambda_config in config['LambdaFunctionConfigurations']: + try: + filter_rules = lambda_config['Filter']['Key']['FilterRules'] + except KeyError: + continue + for filter_rule in filter_rules: + filter_rule['Name'] = filter_rule['Name'].lower() for event_source in event_sources: params = { @@ -217,11 +237,17 @@ def configure_event_source_for_lambda(self, bucket, lambda_arn, } } }) - config['LambdaFunctionConfigurations'].append(params) + # add event notification to remote if it is not already present + for remote_event_source in config['LambdaFunctionConfigurations']: + remote_event_source_copy = remote_event_source.copy() + remote_event_source_copy.pop('Id') + if remote_event_source_copy == params: + break + else: + config['LambdaFunctionConfigurations'].append(params) - self.client.put_bucket_notification_configuration( - Bucket=bucket, - NotificationConfiguration=config) + self.put_bucket_notification( + bucket_name=bucket, notification_configuration=config) def get_list_buckets(self): response = self.client.list_buckets() @@ -316,8 +342,20 @@ def list_objects(self, bucket_name, delimiter=None, encoding_type=None, return bucket_objects def get_bucket_notification(self, bucket_name): - return self.client.get_bucket_notification_configuration( - Bucket=bucket_name + try: + return self.client.get_bucket_notification_configuration( + Bucket=bucket_name + ) + except ClientError as e: + if 'AccessDenied' in str(e): + _LOG.warning(f'{e}. Bucket name - \'{bucket_name}\'.') + else: + raise e + + def put_bucket_notification(self, bucket_name, notification_configuration): + self.client.put_bucket_notification_configuration( + Bucket=bucket_name, + NotificationConfiguration=notification_configuration ) def remove_bucket_notification(self, bucket_name): diff --git a/syndicate/connection/sns_connection.py b/syndicate/connection/sns_connection.py index ddb8bc96..903b4d1b 100644 --- a/syndicate/connection/sns_connection.py +++ b/syndicate/connection/sns_connection.py @@ -256,3 +256,12 @@ def remove_application_by_arn(self, application_arn): """ self.client.delete_platform_application( PlatformApplicationArn=application_arn) + + def list_subscriptions(self): + paginator = self.client.get_paginator('list_subscriptions') + + subscriptions = [] + for page in paginator.paginate(): + subscriptions.extend(page['Subscriptions']) + + return subscriptions diff --git a/syndicate/core/build/deployment_processor.py b/syndicate/core/build/deployment_processor.py index 584478e1..da016f48 100644 --- a/syndicate/core/build/deployment_processor.py +++ b/syndicate/core/build/deployment_processor.py @@ -16,7 +16,6 @@ import concurrent import copy import functools -import json from concurrent.futures import ALL_COMPLETED, ThreadPoolExecutor from functools import cmp_to_key @@ -27,7 +26,6 @@ load_meta_resources, remove_failed_deploy_output, load_latest_deploy_output) -from syndicate.core.build.helper import _json_serial from syndicate.core.build.meta_processor import (resolve_meta, populate_s3_paths, resolve_resource_name) @@ -45,29 +43,9 @@ USER_LOG = get_user_logger() -def get_dependencies(name, meta, resources_dict, resources): - """ Get dependencies from resources that needed to create them too. - - :type name: str - :type meta: dict - :type resources_dict: dict - :param resources: - :param resources_dict: resources that will be created {name: meta} - """ - resources_dict[name] = meta - if meta.get('dependencies'): - for dependency in meta.get('dependencies'): - dep_name = dependency['resource_name'] - dep_meta = resources[dep_name] - resources_dict[dep_name] = dep_meta - if dep_meta.get('dependencies'): - get_dependencies(dep_name, dep_meta, resources_dict, resources) - - -# todo implement resources sorter according to priority -# todo add dependency resolving -def _process_resources(resources, handlers_mapping, pass_context=False): - output = {} +def _process_resources(resources, handlers_mapping, pass_context=False, + output=None): + output = output or {} args = [] resource_type = None try: @@ -110,6 +88,83 @@ def _process_resources(resources, handlers_mapping, pass_context=False): return False, output +def _process_resources_with_dependencies(resources, handlers_mapping, + pass_context=False, + overall_resources=None, output=None, + current_resource_type=None, + run_count=0): + overall_resources = overall_resources or resources + output = output or {} + try: + for res_name, res_meta in resources: + args = [] + resource_type = res_meta['resource_type'] + if res_meta.get('processed'): + _LOG.debug(f"Processing of '{resource_type}' '{res_name}' " + f"skipped. Resource already processed") + continue + + if run_count >= len(overall_resources): + raise RecursionError( + "An infinite loop detected in resource dependencies") + + run_count += 1 + + dependencies = [item['resource_name'] for item in + res_meta.get('dependencies', [])] + _LOG.debug(f"'{resource_type}' '{res_name}' depends on resources: " + f"{dependencies}") + # Order of items in depends_on_resources is important! + depends_on_resources = [] + for dep_res_name in dependencies: + for overall_res_name, overall_res_meta in overall_resources: + if overall_res_name == dep_res_name: + depends_on_resources.append((overall_res_name, + overall_res_meta)) + + if depends_on_resources: + _LOG.info( + f"Processing '{resource_type}' '{res_name}' dependencies " + f"{prettify_json(res_meta['dependencies'])}") + + success, output = _process_resources_with_dependencies( + resources=depends_on_resources, + handlers_mapping=handlers_mapping, + pass_context=pass_context, + overall_resources=overall_resources, + output=output, + current_resource_type=current_resource_type, + run_count=run_count) + + if not success: + return False, output + + args.append(_build_args(name=res_name, + meta=res_meta, + context=output, + pass_context=pass_context)) + if current_resource_type != resource_type: + USER_LOG.info(f'Processing {resource_type} resources') + current_resource_type = resource_type + func = handlers_mapping[resource_type] + response = func(args) + process_response(response=response, output=output) + + res_meta['processed'] = True + overall_res_index = overall_resources.index( + (res_name, res_meta)) + overall_resources[overall_res_index][-1]['processed'] = True + + return True, output + except Exception as e: + if 'An infinite loop' in str(e): + USER_LOG.error(e.args[0]) + else: + USER_LOG.exception(f"Error occurred while '{resource_type}' " + f"resource creating: {str(e)}") + return False, output + + def _build_args(name, meta, context, pass_context=False): """ Builds parameters to pass to resource_type handler. @@ -145,11 +200,37 @@ def update_failed_output(res_name, res_meta, resource_type, output): return output -def deploy_resources(resources): +def deploy_resources(resources, output=None): from syndicate.core import PROCESSOR_FACADE + process_with_dependency = False + + for _, res_meta in resources: + res_priority = DEPLOY_RESOURCE_TYPE_PRIORITY[res_meta['resource_type']] + dependencies = res_meta.get('dependencies', []) + dep_priorities = [ + DEPLOY_RESOURCE_TYPE_PRIORITY[item['resource_type']] for item in + dependencies] + + if dep_priorities: + if max(dep_priorities) >= res_priority: + process_with_dependency = True + break + + if process_with_dependency: + USER_LOG.warn( + 'Resource dependency with higher deployment priority from a ' + 'resource with equal or lower deployment priority detected. ' + 'Deployment may take a little bit more time than usual.') + + return _process_resources_with_dependencies( + resources=resources, + handlers_mapping=PROCESSOR_FACADE.create_handlers(), + output=output) + return _process_resources( resources=resources, - handlers_mapping=PROCESSOR_FACADE.create_handlers()) + handlers_mapping=PROCESSOR_FACADE.create_handlers(), + output=output) def update_resources(resources): @@ -186,56 +267,20 @@ def clean_resources(output): func(args) -# todo implement saving failed output -def continue_deploy_resources(resources, failed_output): - from syndicate.core import PROCESSOR_FACADE - handler_mappings = PROCESSOR_FACADE.create_handlers() - - resources_to_deploy = [] - deploy_result = True - args = [] - - failed_resource_names = {data['resource_name'] for data in - failed_output.values()} +def continue_deploy_resources(resources, failed_output, + project_resources_amount): - for resource_name, resource_meta in resources: - if resource_name not in failed_resource_names: - resources_to_deploy.append((resource_name, resource_meta)) + if len(failed_output) == project_resources_amount: + USER_LOG.info('Skipping deployment because all project resources ' + 'already deployed') + return True, failed_output - if not resources_to_deploy: - return deploy_result, failed_output + for arn, meta in failed_output.items(): + for resource_name, resource_meta in resources: + if resource_name == meta['resource_name']: + resources.remove((resource_name, resource_meta)) - for resource in resources_to_deploy: - res_name, res_meta = resource - res_type = res_meta['resource_type'] - func = handler_mappings[res_type] - try: - if func: - args.append(_build_args( - name=res_name, - meta=res_meta, - context={} - )) - USER_LOG.info(f'Processing {res_type} resources') - response = func(args) - del args[:] - if response: - process_response( - response=response, output=failed_output - ) - else: - _LOG.warning(f"Handler for resource type: {res_type}" - f" did not returned any response") - else: - _LOG.warning(f"Handler for the `{res_name}` resource of" - f" {res_type} type was not found. Resource" - f" has not been deployed.") - except Exception as e: - _LOG.exception( - 'Error occurred while {0} resource creating: {1}'. - format(res_type, str(e))) - deploy_result = False - return deploy_result, failed_output + return deploy_resources(resources, failed_output) def process_response(response, output: dict): @@ -436,10 +481,9 @@ def update_deployment_resources(bundle_name, deploy_name, replace_output=False, prettify_json(resources))) resources_list = list(resources.items()) resources_list.sort(key=cmp_to_key(_compare_update_resources)) - success, output = _process_resources( - resources=resources_list, - handlers_mapping=PROCESSOR_FACADE.update_handlers(), - pass_context=True) + + success, output = update_resources(resources_list) + create_deploy_output(bundle_name=bundle_name, deploy_name=deploy_name, output=output, @@ -532,6 +576,7 @@ def continue_deployment_resources(deploy_name, bundle_name, resources = load_meta_resources(bundle_name) _LOG.debug('{0} file was loaded successfully'.format(BUILD_META_FILE_NAME)) + project_resources_amount = len(resources) resources = resolve_meta(resources) _LOG.debug('Names were resolved') @@ -558,7 +603,8 @@ def continue_deployment_resources(deploy_name, bundle_name, resources_list.sort(key=cmp_to_key(compare_deploy_resources)) success, updated_output = continue_deploy_resources( - resources=resources_list, failed_output=failed_output) + resources=resources_list, failed_output=failed_output, + project_resources_amount=project_resources_amount) _LOG.info('AWS resources were deployed successfully') if success: # apply dynamic changes that uses ARNs diff --git a/syndicate/core/build/meta_processor.py b/syndicate/core/build/meta_processor.py index 2e37b65a..64fe24d3 100644 --- a/syndicate/core/build/meta_processor.py +++ b/syndicate/core/build/meta_processor.py @@ -444,7 +444,8 @@ def create_resource_json(project_path: str, bundle_name: str) -> dict[ # check if all dependencies were described common_validator = VALIDATOR_BY_TYPE_MAPPING[ALL_TYPES] for name, meta in meta_for_validation.items(): - common_validator(resource_meta=meta, all_meta=meta_for_validation) + common_validator(resource_name=name, + resource_meta=meta, all_meta=meta_for_validation) resource_type = meta['resource_type'] type_validator = VALIDATOR_BY_TYPE_MAPPING.get(resource_type) diff --git a/syndicate/core/build/validator/mapping.py b/syndicate/core/build/validator/mapping.py index 643ea759..28a8a6b2 100644 --- a/syndicate/core/build/validator/mapping.py +++ b/syndicate/core/build/validator/mapping.py @@ -24,26 +24,43 @@ from syndicate.core.constants import \ (LAMBDA_CONFIG_FILE_NAME, LAMBDA_TYPE, DYNAMO_TABLE_TYPE, BATCH_COMPENV_TYPE, BATCH_JOBDEF_TYPE, DAX_CLUSTER_TYPE, - EC2_LAUNCH_TEMPLATE_TYPE) + EC2_LAUNCH_TEMPLATE_TYPE, RESOURCE_LIST) ALL_TYPES = 'all_types' _LOG = get_logger('validator') -def common_validate(resource_meta, all_meta): +def common_validate(resource_name, resource_meta, all_meta): dependencies = resource_meta.get('dependencies') if dependencies: - for dependency in resource_meta['dependencies']: - dependency_name = dependency.get('resource_name') - if dependency_name not in list(all_meta.keys()): - err_mess = ("One of resource dependencies wasn't " - "described: {0}. Please, describe this " - "resource in {1} if it is Lambda or in " - "deployment_resources.json" - .format(dependency_name, - LAMBDA_CONFIG_FILE_NAME)) - raise AssertionError(err_mess) + for dependency in dependencies: + errors = [] + + if not dependency.get('resource_name'): + errors.append( + f"There is no 'resource_name' in resource " + f"'{resource_name}' dependency {dependency}") + elif dependency.get('resource_name') not in list(all_meta.keys()): + errors.append( + f"The resource '{resource_name}' depends on resource " + f"'{dependency.get('resource_name')}' that is not a part " + f"of the project. Please double-check the project " + f"resources description add it to the project or remove " + f"it from the resource '{resource_name}' dependencies.") + + if not dependency.get('resource_type'): + errors.append( + f"There is no 'resource_type' in resource " + f"'{resource_name}' dependency {dependency}") + elif dependency.get('resource_type') not in RESOURCE_LIST: + errors.append( + f"Unsupported resource type " + f"'{dependency.get('resource_type')}' found in " + f"the resource '{resource_name}' dependency " + f"'{dependency}'.") + if errors: + raise AssertionError(str(errors)) # validation customization diff --git a/syndicate/core/decorators.py b/syndicate/core/decorators.py index 599c5eb1..6d38ad94 100644 --- a/syndicate/core/decorators.py +++ b/syndicate/core/decorators.py @@ -13,6 +13,7 @@ See the License for the specific language governing permissions and limitations under the License. """ +import threading from functools import wraps from pathlib import PurePath import click @@ -21,6 +22,8 @@ _LOG = get_logger('syndicate.core.decorators') +lock = threading.Lock() + def check_deploy_name_for_duplicates(func): """ @@ -72,3 +75,13 @@ def real_wrapper(*args, **kwargs): return func(*args, **kwargs) return real_wrapper + + +def threading_lock(func): + """ Synchronize access to a function with a threading lock + to avoid race condition.""" + @wraps(func) + def wrapper(*args, **kwargs): + with lock: + return func(*args, **kwargs) + return wrapper diff --git a/syndicate/core/generators/contents.py b/syndicate/core/generators/contents.py index ca78770e..1f179217 100644 --- a/syndicate/core/generators/contents.py +++ b/syndicate/core/generators/contents.py @@ -41,7 +41,8 @@ import java.util.HashMap; import java.util.Map; -@LambdaHandler(lambdaName = "{lambda_name}", +@LambdaHandler( + lambdaName = "{lambda_name}", roleName = "{lambda_role_name}", isPublishVersion = true, aliasName = "${lambdas_alias_name}", @@ -71,7 +72,7 @@ 3.5.2 - 1.11.1 + 1.13.0 11 11 UTF-8 diff --git a/syndicate/core/handlers.py b/syndicate/core/handlers.py index 831e0d67..25d3c8a4 100644 --- a/syndicate/core/handlers.py +++ b/syndicate/core/handlers.py @@ -16,6 +16,7 @@ import json import os import sys +from functools import partial import click from tabulate import tabulate @@ -61,7 +62,8 @@ resolve_default_value, ValidRegionParamType, generate_default_bundle_name, resolve_and_verify_bundle_callback, - param_to_lower, verbose_option) + param_to_lower, verbose_option, + validate_incompatible_options) from syndicate.core.project_state.project_state import (MODIFICATION_LOCK, WARMUP_LOCK) from syndicate.core.project_state.status_processor import project_state_status @@ -220,11 +222,13 @@ def transform(bundle_name, dsl, output_dir): 'while deploy') @click.option('--excluded_types', '-extypes', multiple=True, help='Types of the resources to skip while deploy') -@click.option('--continue_deploy', is_flag=True, +@click.option('--continue_deploy', is_flag=True, is_eager=True, help='Flag to continue failed deploy') @click.option('--replace_output', is_flag=True, default=False, help='Flag to replace the existing deploy output') @click.option('--rollback_on_error', is_flag=True, default=False, + callback=partial(validate_incompatible_options, + incompatible_options=['continue_deploy']), help='Flag to automatically clean deployed resources if the' ' deployment is unsuccessful. Cannot be used with' ' --continue_deploy flag.') @@ -721,6 +725,11 @@ def assemble_swagger_ui(**kwargs): @click.option('--bundle_name', '-b', callback=generate_default_bundle_name, help='Bundle\'s name to build the lambdas in. ' 'Default value: $ProjectName_%Y%m%d.%H%M%S') +@click.option('--force_upload', '-fu', nargs=1, + callback=resolve_path_callback, default=False, + help='Identifier that indicates whether a locally existing' + ' bundle should be deleted and a new one created using' + ' the same path.') @verbose_option @click.pass_context @timeit(action_name=ASSEMBLE_ACTION) diff --git a/syndicate/core/project_state/project_state.py b/syndicate/core/project_state/project_state.py index 7ce6c293..fe1a7613 100644 --- a/syndicate/core/project_state/project_state.py +++ b/syndicate/core/project_state/project_state.py @@ -246,7 +246,7 @@ def is_lock_free(self, lock_name): elif locked_till := lock.get(LOCK_LOCKED_TILL): locked_till_datetime = datetime.strptime( locked_till, DATE_FORMAT_ISO_8601) - if datetime.timestamp(locked_till_datetime) <= time.time(): + if locked_till_datetime <= datetime.utcnow(): lock[LOCK_LOCKED_TILL] = None return True return False @@ -408,7 +408,7 @@ def __modify_lock_state(self, lock_name, locked): locks = self.locks lock = locks.get(lock_name) - modification_datetime = datetime.fromtimestamp(time.time()) + modification_datetime = datetime.utcnow() timestamp = modification_datetime.strftime(DATE_FORMAT_ISO_8601) locked_till_timestamp = (modification_datetime + timedelta(minutes=locked_till)).strftime( diff --git a/syndicate/core/resources/api_gateway_resource.py b/syndicate/core/resources/api_gateway_resource.py index 4df41761..4fcf5d29 100644 --- a/syndicate/core/resources/api_gateway_resource.py +++ b/syndicate/core/resources/api_gateway_resource.py @@ -321,11 +321,11 @@ def _create_api_gateway_from_meta(self, name, meta): api_resources = meta['resources'] # whether to put a wildcard in lambda resource-based policy permissions resources_permission_singleton = meta.get(POLICY_STATEMENT_SINGLETON) - # api_gw_describe = self.describe_api_resources(name, meta) - # if api_gw_describe: - # _LOG.info(f'Api gateway with name \'{name}\' exists. Returning') - # return api_gw_describe - # _LOG.info(f'Api gateway with name \'{name}\' does not exist. Creating') + api_gw_describe = self.describe_api_resources(name, meta) + if api_gw_describe: + _LOG.info(f'Api gateway with name \'{name}\' exists. Returning') + return api_gw_describe + _LOG.info(f'Api gateway with name \'{name}\' does not exist. Creating') api_item = self.connection.create_rest_api( api_name=name, binary_media_types=meta.get('binary_media_types')) @@ -413,6 +413,12 @@ def _create_api_gateway_openapi_from_meta(self, name, meta): self._resolve_cup_ids(openapi_context) + api_gw_describe = self.describe_api_resources(name, meta) + if api_gw_describe: + _LOG.info(f'Api gateway with name \'{name}\' exists. Returning') + return api_gw_describe + + _LOG.info(f'Api gateway with name \'{name}\' does not exist. Creating') api_id = self.connection.create_openapi(openapi_context) self.connection.deploy_api(api_id, deploy_stage) diff --git a/syndicate/core/resources/lambda_resource.py b/syndicate/core/resources/lambda_resource.py index 7c386b1e..fea6c7a1 100644 --- a/syndicate/core/resources/lambda_resource.py +++ b/syndicate/core/resources/lambda_resource.py @@ -24,6 +24,7 @@ from syndicate.connection.helper import retry from syndicate.core.build.meta_processor import S3_PATH_NAME from syndicate.core.constants import DEFAULT_LOGS_EXPIRATION +from syndicate.core.decorators import threading_lock from syndicate.core.helper import (unpack_kwargs, exit_on_exception) from syndicate.core.resources.base_resource import BaseResource @@ -63,13 +64,14 @@ SNS_TOPIC_TRIGGER = 'sns_topic_trigger' KINESIS_TRIGGER = 'kinesis_trigger' SQS_TRIGGER = 'sqs_trigger' +NOT_AVAILABLE = 'N/A' class LambdaResource(BaseResource): def __init__(self, lambda_conn, s3_conn, cw_logs_conn, sns_conn, iam_conn, dynamodb_conn, sqs_conn, kinesis_conn, - cw_events_conn, region, account_id, + cw_events_conn, cognito_idp_conn, region, account_id, deploy_target_bucket) -> None: self.lambda_conn = lambda_conn self.s3_conn = s3_conn @@ -80,10 +82,18 @@ def __init__(self, lambda_conn, s3_conn, cw_logs_conn, sns_conn, self.sqs_conn = sqs_conn self.kinesis_conn = kinesis_conn self.cw_events_conn = cw_events_conn + self.cognito_idp_conn = cognito_idp_conn self.region = region self.account_id = account_id self.deploy_target_bucket = deploy_target_bucket + self.dynamic_params_resolvers = { + ('cognito_idp', 'id'): + self.cognito_idp_conn.if_pool_exists_by_name, + ('cognito_idp', 'client_id'): + self.cognito_idp_conn.if_cup_client_exist + } + def qualifier_alias_resolver(self, lambda_def): return lambda_def['Alias'] @@ -94,6 +104,23 @@ def remove_permissions(self, lambda_arn, permissions_sids): return self.lambda_conn.remove_permissions(lambda_arn, permissions_sids) + def remove_permissions_by_resource_name(self, lambda_name, resource_name): + """ Remove permissions to invoke lambda by resource name + + :param lambda_name: lambda name, arn or full arn + :param resource_name: resource name, arn or full arn + """ + lambda_permissions = self.get_existing_permissions(lambda_name) + for statement in lambda_permissions: + try: + source_arn = statement['Condition']['ArnLike']['AWS:SourceArn'] + except KeyError: + continue + if resource_name in source_arn: + self.lambda_conn.remove_one_permission( + function_name=lambda_name, + statement_id=statement['Sid']) + def qualifier_version_resolver(self, lambda_def): latest_version_number = lambda_def['Configuration']['Version'] if 'LATEST' in latest_version_number: @@ -301,6 +328,9 @@ def _create_lambda_from_meta(self, name, meta): ephemeral_storage = meta.get('ephemeral_storage', 512) + if meta.get('env_variables'): + self._resolve_env_variables(meta.get('env_variables')) + self.lambda_conn.create_lambda( lambda_name=name, func_name=meta['func_name'], @@ -483,6 +513,9 @@ def _update_lambda(self, name, meta, context): 'due to layer absence!'.format(layer_name, name)) lambda_layers_arns.append(layer_arn) + if env_vars: + self._resolve_env_variables(env_vars) + _LOG.info(f'Updating lambda {name} configuration') self.lambda_conn.update_lambda_configuration( lambda_name=name, role=role_arn, handler=handler, @@ -553,12 +586,15 @@ def _update_lambda(self, name, meta, context): self.lambda_conn.delete_url_config( function_name=name, qualifier=alias_name) + arn = self.build_lambda_arn_with_alias(response, alias_name) \ + if publish_version or alias_name else \ + response['Configuration']['FunctionArn'] + + # delete lambda triggers before update for clean triggers configuration + self.remove_all_lambda_triggers(name, arn) + if meta.get('event_sources'): event_sources_meta = meta['event_sources'] - if alias_name: - _arn = self.build_lambda_arn_with_alias(response, alias_name) - else: - _arn = response['Configuration']['FunctionArn'] trigger_resource_types = set( event_source['resource_type'] for event_source in event_sources_meta @@ -571,11 +607,11 @@ def _update_lambda(self, name, meta, context): func = self.CREATE_TRIGGER[trigger_type] # process s3 event sources in batch if trigger_type == S3_TRIGGER: - func(self, name, _arn, role_name, event_sources_by_type) + func(self, name, arn, role_name, event_sources_by_type) # process other event sources one by one else: for event_source in event_sources_by_type: - func(self, name, _arn, role_name, event_source) + func(self, name, arn, role_name, event_source) if meta.get('max_retries') is not None: _LOG.debug('Updating lambda event invoke config') @@ -813,6 +849,8 @@ def _create_sqs_trigger_from_meta(self, lambda_name, lambda_arn, role_name, trigger_meta): validate_params(lambda_name, trigger_meta, SQS_TRIGGER_REQUIRED_PARAMS) target_queue = trigger_meta['target_queue'] + function_response_types = trigger_meta.get( + "function_response_types", []) batch_size, batch_window = self._resolve_batch_size_batch_window( trigger_meta) @@ -833,10 +871,12 @@ def _create_sqs_trigger_from_meta(self, lambda_name, lambda_arn, role_name, self.lambda_conn.update_event_source( event_source['UUID'], function_name=lambda_arn, batch_size=batch_size, - batch_window=batch_window) + batch_window=batch_window, + function_response_types=function_response_types) else: self.lambda_conn.add_event_source( - lambda_arn, queue_arn, batch_size, batch_window + lambda_arn, queue_arn, batch_size, batch_window, + function_response_types=function_response_types ) _LOG.info('Lambda %s subscribed to SQS queue %s', lambda_name, @@ -978,9 +1018,10 @@ def _create_kinesis_stream_trigger_from_meta(self, lambda_name, lambda_arn, @retry() def _add_kinesis_event_source(self, lambda_name, stream_arn, trigger_meta): - self.lambda_conn.add_event_source(lambda_name, stream_arn, - trigger_meta['batch_size'], - trigger_meta['starting_position']) + self.lambda_conn.add_event_source( + func_name=lambda_name, stream_arn=stream_arn, + batch_size=trigger_meta['batch_size'], + start_position=trigger_meta['starting_position']) CREATE_TRIGGER = { DYNAMO_DB_TRIGGER: _create_dynamodb_trigger_from_meta, @@ -992,6 +1033,86 @@ def _add_kinesis_event_source(self, lambda_name, stream_arn, trigger_meta): SQS_TRIGGER: _create_sqs_trigger_from_meta } + def remove_all_lambda_triggers(self, lambda_name, lambda_arn): + # event sources (sqs, dynamodb streams, kinesis, kafka, amazon mq) + self.lambda_conn.remove_trigger(lambda_arn) + + self._remove_s3_triggers(lambda_name, lambda_arn) + self._remove_cloud_watch_triggers(lambda_name, lambda_arn) + self._remove_sns_triggers(lambda_name, lambda_arn) + + def _remove_sns_triggers(self, lambda_name, lambda_arn): + subscriptions = self.sns_conn.list_subscriptions() + + for subscription in subscriptions: + topic_name = subscription['TopicArn'].split(':')[-1] + if subscription['Protocol'] == 'lambda' \ + and subscription['Endpoint'] == lambda_arn: + self.sns_conn.unsubscribe_arn( + subscription_arn=subscription['SubscriptionArn']) + _LOG.info(f'Lambda {lambda_name} unsubscribed ' + f'from topic {topic_name}') + + # remove sns permission to invoke lambda + # to remove this trigger from lambda triggers section + self.remove_permissions_by_resource_name( + lambda_arn, subscription['TopicArn']) + + def _remove_cloud_watch_triggers(self, lambda_name, lambda_arn): + rule_names = self.cw_events_conn.list_rules_by_target(lambda_arn) + + for rule_name in rule_names: + targets = self.cw_events_conn.list_targets_by_rule(rule_name) + + # remove target so that this rule won't trigger lambda + for target in targets: + if target['Arn'] == lambda_arn: + self.cw_events_conn.remove_targets( + rule_name=rule_name, + target_ids=[target['Id']] + ) + _LOG.info(f'Lambda {lambda_name} unsubscribed from ' + f'cloudwatch rule {rule_name}') + + # remove event bridge permission to invoke lambda + # to remove this trigger from lambda triggers section + self.remove_permissions_by_resource_name(lambda_arn, rule_name) + + @threading_lock + def _remove_s3_triggers(self, lambda_name, lambda_arn): + buckets = self.s3_conn.get_list_buckets() or [] + for bucket in buckets: + bucket_notifications = self.s3_conn.get_bucket_notification( + bucket_name=bucket['Name']) + if not bucket_notifications: + continue + bucket_notifications.pop('ResponseMetadata') + if 'LambdaFunctionConfigurations' in bucket_notifications: + lambda_configs = \ + bucket_notifications['LambdaFunctionConfigurations'] + lambda_config_arns = [lambda_config['LambdaFunctionArn'] + for lambda_config in lambda_configs] + if lambda_arn in lambda_config_arns: + # exclude current lambda from the s3 event config + saved_configs = [ + lambda_config for lambda_config in lambda_configs + if lambda_config['LambdaFunctionArn'] != lambda_arn + ] + bucket_notifications['LambdaFunctionConfigurations'] = \ + saved_configs + self.s3_conn.put_bucket_notification( + bucket_name=bucket['Name'], + notification_configuration=bucket_notifications + ) + + # remove s3 permission to invoke lambda + # to remove this trigger from lambda triggers section + self.remove_permissions_by_resource_name( + lambda_arn, bucket['Name']) + + _LOG.info(f'Lambda {lambda_name} unsubscribed from ' + f's3 bucket {bucket["Name"]}') + def remove_lambdas(self, args): self.create_pool(self._remove_lambda, args) @@ -1001,7 +1122,7 @@ def _remove_lambda(self, arn, config): lambda_name = config['resource_name'] try: self.lambda_conn.delete_lambda(lambda_name) - self.lambda_conn.remove_trigger(arn) + self.remove_all_lambda_triggers(lambda_name, arn) group_names = self.cw_logs_conn.get_log_group_names() for each in group_names: if lambda_name == each.split('/')[-1]: @@ -1159,4 +1280,55 @@ def _resolve_log_group(self, lambda_name: str, meta: dict): self.cw_logs_conn.create_log_group_with_retention_days( group_name=lambda_name, retention_in_days=retention - ) \ No newline at end of file + ) + + def _resolve_env_variables(self, env_vars): + required_params = ['resource_name', 'resource_type', 'parameter'] + + for key, value in env_vars.items(): + if isinstance(value, dict): + resource_name = value.get('resource_name') + resource_type = value.get('resource_type') + parameter = value.get('parameter') + + if not all([resource_name, resource_type, parameter]): + missed_params = [p for p in required_params if + value.get(p) is None] + env_vars[key] = NOT_AVAILABLE + USER_LOG.warn( + f"Unable to resolve value for environment variable " + f"'{key}' because of missing parameter/s. Required " + f"parameters: {required_params}; missed parameters/s " + f"{missed_params}." + f"The environment variable '{key}' will be configured " + f"with the value '{NOT_AVAILABLE}'." + ) + continue + + _LOG.debug( + f"Going to resolve the value for the environment variable " + f"'{key}' by the parameter '{parameter}' of the resource " + f"type '{resource_type}' with the name '{resource_name}'.") + + resolver = self.dynamic_params_resolvers.get( + (resource_type, parameter) + ) + + if resolver is None: + USER_LOG.warn( + f"Currently resolving parameter '{parameter}' for the " + f"resource type '{resource_type}' is not supported.") + env_vars[key] = NOT_AVAILABLE + else: + env_vars[key] = (resolver(resource_name) or NOT_AVAILABLE) + + if env_vars[key] == NOT_AVAILABLE: + USER_LOG.warn( + f"Unable to resolve parameter '{parameter}' for the " + f"resource type '{resource_type}' with name " + f"'{resource_name}'.") + + _LOG.debug( + f"The environment variable '{key}' will be configured " + f"with the value '{env_vars[key]}'." + ) diff --git a/syndicate/core/resources/resources_provider.py b/syndicate/core/resources/resources_provider.py index 9a8425a7..001da513 100644 --- a/syndicate/core/resources/resources_provider.py +++ b/syndicate/core/resources/resources_provider.py @@ -249,6 +249,8 @@ def lambda_resource(self): sqs_conn=self._conn_provider.sqs(), kinesis_conn=self._conn_provider.kinesis(), cw_events_conn=self._conn_provider.cw_events(), + cognito_idp_conn= + self._conn_provider.cognito_identity_provider(), region=self.config.region, account_id=self.config.account_id, deploy_target_bucket=self.config.deploy_target_bucket diff --git a/syndicate/core/resources/sns_resource.py b/syndicate/core/resources/sns_resource.py index 22f22e0e..fe552064 100644 --- a/syndicate/core/resources/sns_resource.py +++ b/syndicate/core/resources/sns_resource.py @@ -248,6 +248,13 @@ def _remove_sns_topic_subscriptions(self, topic_arn): self.connection_provider.sns(region).unsubscribe( subscription_arn) + def unsubscribe_arn(self, subscription_arn): + self.connection_provider.sns().unsubscribe( + subscription_arn=subscription_arn) + + def list_subscriptions(self): + return self.connection_provider.sns().list_subscriptions() + @unpack_kwargs def _create_platform_application_from_meta(self, name, meta, region): required_parameters = ['platform', 'attributes']