diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..3ca78de --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @muhamadto \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..bd3c017 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,29 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Versions** + +**Additional context** +Add any other context about the problem here. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..24473de --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. \ No newline at end of file diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..1c59b0b --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +# Licensed to Muhammad Hamadto + +# 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.# To get started with Dependabot version updates, you'll need to specify which + + +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..ed8dc08 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,53 @@ +# Licensed to Muhammad Hamadto +# +# 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 +# +# See the NOTICE file distributed with this work for additional information regarding copyright ownership. +# +# 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. + +name: "Build" + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + types: [ opened, synchronize, reopened ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' + - name: Cache Sonar packages + uses: actions/cache@v3 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + - name: Cache Maven packages + uses: actions/cache@v3 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Build with Maven + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: mvn --no-transfer-progress clean verify \ No newline at end of file diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..307767b --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,72 @@ +# Licensed to Muhammad Hamadto +# +# 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 +# +# See the NOTICE file distributed with this work for additional information regarding copyright ownership. +# +# 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. + +name: "CodeQL" + +on: + push: + branches: [ "*" ] + pull_request: + branches: [ "main" ] + types: [ opened, synchronize, reopened ] + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + language: [ 'java' ] + steps: + - name: Checkout repository + uses: actions/checkout@v3 + - name: Setup Java + uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '21' + cache: 'maven' + - name: Initialize CodeQL + uses: github/codeql-action/init@v2 + with: + languages: ${{ matrix.language }} + - name: maven-settings-xml-action + uses: whelk-io/maven-settings-xml-action@v18 + with: + repositories: > + [ + { + "id": "spring-releases", + "name": "Spring Releases", + "url": "https://repo.spring.io/release" + } + ] + plugin_repositories: > + [ + { + "id": "spring-releases", + "name": "Spring Releases", + "url": "https://repo.spring.io/release" + } + ] + - name: Autobuild + uses: github/codeql-action/autobuild@v2 + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v2 \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1056af9 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,37 @@ +# Licensed to Muhammad Hamadto +# +# 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 +# +# See the NOTICE file distributed with this work for additional information regarding copyright ownership. +# +# 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. + +name: Publish package to Maven Central +on: + release: + types: [ published ] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + - name: Publish package + run: mvn --batch-mode deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b425f09 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.mvn/MavenWrapperDownloader.java b/.mvn/MavenWrapperDownloader.java new file mode 100644 index 0000000..4187c8d --- /dev/null +++ b/.mvn/MavenWrapperDownloader.java @@ -0,0 +1,117 @@ +/* + * Copyright 2007-present the original author or authors. + * + * 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 + * + * https://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 java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if(mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if(mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if(!outputFile.getParentFile().exists()) { + if(!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} \ No newline at end of file diff --git a/.mvn/maven-wrapper.properties b/.mvn/maven-wrapper.properties new file mode 100644 index 0000000..a1af7c5 --- /dev/null +++ b/.mvn/maven-wrapper.properties @@ -0,0 +1,34 @@ +# +# Licensed to Muhammad Hamadto +# +# 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 +# +# See the NOTICE file distributed with this work for additional information regarding copyright ownership. +# +# 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. +# + +# +# Licensed to Muhammad Hamadto +# +# 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. +# +# +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..53eb30c --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +muhamadto@gmail.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..605698c --- /dev/null +++ b/LICENSE @@ -0,0 +1,15 @@ +Licensed to Muhammad Hamadto + +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 +https://www.apache.org/licenses/LICENSE-2.0 + +See the NOTICE file distributed with this work for additional information regarding +copyright ownership. + +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. \ No newline at end of file diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..e69de29 diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..71c43c0 --- /dev/null +++ b/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ +## Hello, Please Review My Pull Request! + + + +#### :heavy_check_mark: Checklist + + + +- [ ] Describe what you did in the pull request description +- [ ] Add Unit and Integration Tests - at least 80% unit tests for new code. +- [ ] Added or updated documentation \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..66df285 --- /dev/null +++ b/mvnw @@ -0,0 +1,308 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Apache Maven Wrapper startup batch script, version 3.2.0 +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "$(uname)" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME + else + JAVA_HOME="/Library/Java/Home"; export JAVA_HOME + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=$(java-config --jre-home) + fi +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --unix "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --unix "$CLASSPATH") +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] && + JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="$(which javac)" + if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=$(which readlink) + if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then + if $darwin ; then + javaHome="$(dirname "\"$javaExecutable\"")" + javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac" + else + javaExecutable="$(readlink -f "\"$javaExecutable\"")" + fi + javaHome="$(dirname "\"$javaExecutable\"")" + javaHome=$(expr "$javaHome" : '\(.*\)/bin') + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=$(cd "$wdir/.." || exit 1; pwd) + fi + # end of workaround + done + printf '%s' "$(cd "$basedir" || exit 1; pwd)" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + # Remove \r in case we run on Windows within Git Bash + # and check out the repository with auto CRLF management + # enabled. Otherwise, we may read lines that are delimited with + # \r\n and produce $'-Xarg\r' rather than -Xarg due to word + # splitting rules. + tr -s '\r\n' ' ' < "$1" + fi +} + +log() { + if [ "$MVNW_VERBOSE" = true ]; then + printf '%s\n' "$1" + fi +} + +BASE_DIR=$(find_maven_basedir "$(dirname "$0")") +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR +log "$MAVEN_PROJECTBASEDIR" + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" +if [ -r "$wrapperJarPath" ]; then + log "Found $wrapperJarPath" +else + log "Couldn't find $wrapperJarPath, downloading it ..." + + if [ -n "$MVNW_REPOURL" ]; then + wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + else + wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar" + fi + while IFS="=" read -r key value; do + # Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' ) + safeValue=$(echo "$value" | tr -d '\r') + case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;; + esac + done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" + log "Downloading from: $wrapperUrl" + + if $cygwin; then + wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath") + fi + + if command -v wget > /dev/null; then + log "Found wget ... using wget" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + log "Found curl ... using curl" + [ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent" + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + else + curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath" + fi + else + log "Falling back to using Java to download" + javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java" + javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaSource=$(cygpath --path --windows "$javaSource") + javaClass=$(cygpath --path --windows "$javaClass") + fi + if [ -e "$javaSource" ]; then + if [ ! -e "$javaClass" ]; then + log " - Compiling MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/javac" "$javaSource") + fi + if [ -e "$javaClass" ]; then + log " - Running MavenWrapperDownloader.java ..." + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath" + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +# If specified, validate the SHA-256 sum of the Maven wrapper jar file +wrapperSha256Sum="" +while IFS="=" read -r key value; do + case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;; + esac +done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties" +if [ -n "$wrapperSha256Sum" ]; then + wrapperSha256Result=false + if command -v sha256sum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + elif command -v shasum > /dev/null; then + if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then + wrapperSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." + echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties." + exit 1 + fi + if [ $wrapperSha256Result = false ]; then + echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2 + echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2 + echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2 + exit 1 + fi +fi + +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$JAVA_HOME" ] && + JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME") + [ -n "$CLASSPATH" ] && + CLASSPATH=$(cygpath --path --windows "$CLASSPATH") + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR") +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +# shellcheck disable=SC2086 # safe args +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..2f8b90b --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. You may obtain a copy of the License at +@REM +@REM https://www.apache.org/licenses/LICENSE-2.0 +@REM +@REM Unless required by applicable law or agreed to in writing, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..22f6459 --- /dev/null +++ b/pom.xml @@ -0,0 +1,207 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + pom + + sandpipers-cdk-constructs + sandpipers-cdk-assertions + sandpipers-cdk-examples + sandpipers-cdk-types + sandpipers-cdk-bom + + + + 21 + 21 + UTF-8 + + 2.128.0 + 5.10.0 + + + + + github + GitHub Packages + https://maven.pkg.github.com/muhamadto/sandpipers-cdk + + + + + + + + io.sandpipers + sandpipers-cdk-constructs + ${project.version} + + + + io.sandpipers + sandpipers-cdk-assertions + ${project.version} + + + + io.sandpipers + sandpipers-cdk-types + ${project.version} + + + + + software.amazon.awscdk + aws-cdk-lib + ${aws-cdk.version} + provided + + + + software.amazon.awscdk + apprunner-alpha + ${aws-cdk.version}-alpha.0 + + + + + + org.apache.commons + commons-lang3 + 3.14.0 + + + + org.apache.commons + commons-collections4 + 4.4 + + + + javax.validation + validation-api + 2.0.1.Final + + + + com.google.guava + guava + 33.0.0-jre + + + + org.projectlombok + lombok + 1.18.30 + + + + org.typefactory + type-factory-bom + 1.0.0 + pom + import + + + + com.fasterxml.jackson + jackson-bom + 2.17.0 + pom + import + + + + + + org.assertj + assertj-core + 3.25.3 + + + + org.junit.jupiter + junit-jupiter-api + 5.10.2 + test + + + + org.mockito + mockito-core + ${mockito-core.version} + test + + + + org.mockito + mockito-junit-jupiter + ${mockito-core.version} + test + + + + org.testcontainers + testcontainers-bom + 1.19.0 + pom + import + + + + + + + + matto + Muhammad Hamadto + https://github.com/muhamadto + + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + Licensed to Muhammad Hamadto + + 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 + https://www.apache.org/licenses/LICENSE-2.0 + + See the NOTICE file distributed with this work for additional information regarding + copyright ownership. + + 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. + + + + \ No newline at end of file diff --git a/sandpipers-cdk-assertions/README.md b/sandpipers-cdk-assertions/README.md new file mode 100644 index 0000000..d4e09d2 --- /dev/null +++ b/sandpipers-cdk-assertions/README.md @@ -0,0 +1,117 @@ +# sandpipers-cdk-assertions + +AssertJ-like fluent assertions for AWS CDK testing + +### AWS CDK testing framework + +AWS offers a framework to verify that your stack is well-formed and that it matches your expectations. This framework can be found [here](https://docs.aws.amazon.com/cdk/latest/guide/testing.html). + +Here is an example to very the stack contains a specific role: + + +```java + +template.hasResourceProperties("AWS::IAM::Role", Match.objectEquals( + Collections.singletonMap("AssumeRolePolicyDocument", Map.of( + "Version", "2012-10-17", + "Statement", Collections.singletonList(Map.of( + "Action", "sts:AssumeRole", + "Effect", "Allow", + "Principal", Collections.singletonMap( + "Service", Collections.singletonMap( + "Fn::Join", Arrays.asList( + "", + Arrays.asList("states.", Match.anyValue(), ".amazonaws.com") + ) + ) + ) + )) + )) +)); +``` + +### AWS CDK fluent testing library +I noticed that the tests written using the AWS framework were verbose and presented challenges in terms of readability and maintainability. In response, I decided to create a small library to make it easier to read, write and maintain tests. + +To illustrate, an equivalent representation to the aforementioned example can be found below: as follows" +```java +CDKStackAssert.assertThat(template) + .containsRoleWithManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument("states.amazonaws.com", null, "Allow", "2012-10-17", "sts:AssumeRole"); +``` + +#### How to use it + +* The library is available on Maven Central. To use it, add the following dependency to your project: + +```xml + + io.sandpipers + sandpipers-cdk-assertions + test + +``` + +* Create a class `TemplateSupport` in your testing packages. This class will be used to load the template and create the `CDKStackAssert` object. + +```java +public abstract class TemplateSupport { + + public static final String ENV = "test"; + public static final String TEST_CDK_BUCKET = "test-cdk-bucket"; + public static final String QUALIFIER = "test"; + static Template template; + private static final String STACK_NAME = "example-lambda-function-test-stack"; + + @TempDir + private static Path TEMP_DIR; + + @BeforeAll + static void initAll() throws IOException { + final Path lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); + + final Map tags = createTags(ENV, TAG_VALUE_COST_CENTRE); + final App app = new App(); + final ExampleLambdaStack stack = StackUtils.createStack(app, STACK_NAME, lambdaCodePath.toString(), QUALIFIER, TEST_CDK_BUCKET, ENV); + + tags.entrySet().stream() + .filter(tag -> Objects.nonNull(tag.getValue())) + .forEach(tag -> Tags.of(app).add(tag.getKey(), tag.getValue())); + + template = Template.fromStack(stack); + } + + @AfterAll + static void cleanup() { + template = null; + } +} +``` +* Extend the `TemplateSupport` class in your test classes and use the `CDKStackAssert` object to verify your stack. + +```java +class LambdaTest extends TemplateSupport { + + public static final String TEST = "test"; + + @Test + void should_have_lambda_function() { + + CDKStackAssert.assertThat(template) + .containsFunction("example-lambda-function") + .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .hasCode("test-cdk-bucket", "(.*).zip") + .hasRole("examplelambdafunctionrole(.*)") + .hasDependency("examplelambdafunctionrole(.*)") + .hasDependency("examplelambdafunctionroleDefaultPolicy(.*)") + .hasTag("COST_CENTRE", TAG_VALUE_COST_CENTRE) + .hasTag("ENV", TEST) + .hasEnvironmentVariable("ENV", TEST) + .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST) + .hasDescription("Lambda example") + .hasMemorySize(512) + .hasRuntime("provided.al2") + .hasTimeout(3); + } +} +``` \ No newline at end of file diff --git a/sandpipers-cdk-assertions/pom.xml b/sandpipers-cdk-assertions/pom.xml new file mode 100644 index 0000000..fadd4a0 --- /dev/null +++ b/sandpipers-cdk-assertions/pom.xml @@ -0,0 +1,81 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + + + sandpipers-cdk-assertions + + + 21 + 21 + UTF-8 + + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + software.amazon.awscdk + aws-cdk-lib + + + + software.amazon.awscdk + apprunner-alpha + + + + + + org.apache.commons + commons-lang3 + + + + org.projectlombok + lombok + + + + + + + org.assertj + assertj-core + + + org.apache.commons + commons-collections4 + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java new file mode 100644 index 0000000..33c48ac --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/AbstractCDKResourcesAssert.java @@ -0,0 +1,247 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.InstanceOfAssertFactories; + +@SuppressWarnings("unchecked") +public abstract class AbstractCDKResourcesAssert, ACTUAL extends Map> extends + AbstractAssert { + + protected AbstractCDKResourcesAssert(ACTUAL actual, Class selfType) { + super(actual, selfType); + } + + public SELF hasTag(final String key, final Object value) { + hasTag("Tags", key, value); + return myself; + } + + public SELF hasTag(final String tagsMapId, + final String key, + final Object value) { + + final Map> properties = + (Map>) actual.get("Properties"); + + final List tags = (List) properties.get(tagsMapId); + + Assertions.assertThat(tags) + .isNotEmpty() + .contains(Map.of("Key", key, "Value", value)); + return myself; + } + + public SELF hasDependency(final String expected) { + + final List actualDependency = (List) actual.get("DependsOn"); + + Assertions.assertThat(actualDependency) + .isInstanceOf(List.class) + .anyMatch(s -> s.matches(expected)); + + return myself; + } + + public SELF hasUpdateReplacePolicy(final String expected) { + + final String actualUpdateReplacePolicy = (String) actual.get("UpdateReplacePolicy"); + + Assertions.assertThat(actualUpdateReplacePolicy) + .isInstanceOf(String.class) + .isEqualTo(expected); + + return myself; + } + + public SELF hasDeletionPolicy(final String expected) { + + final String actualDeletionPolicy = (String) actual.get("DeletionPolicy"); + + Assertions.assertThat(actualDeletionPolicy) + .isInstanceOf(String.class) + .isEqualTo(expected); + + return myself; + } + + public SELF hasEnvironmentVariable(final String key, final String value) { + final Map properties = (Map) actual.get("Properties"); + final Map environment = (Map) properties.get("Environment"); + final Map variables = (Map) environment.get("Variables"); + + Assertions.assertThat(variables.get(key)) + .isInstanceOf(String.class) + .isEqualTo(value); + + return myself; + } + + public SELF hasDescription(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String description = (String) properties.get("Description"); + + Assertions.assertThat(description) + .isInstanceOf(String.class) + .isEqualTo(expected); + + return myself; + } + + public SELF hasMemorySize(final Integer expected) { + final Map properties = (Map) actual.get("Properties"); + final Integer memorySize = (Integer) properties.get("MemorySize"); + + Assertions.assertThat(memorySize) + .isInstanceOf(Integer.class) + .isEqualTo(expected); + + return myself; + } + + public SELF hasRuntime(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String runtime = (String) properties.get("Runtime"); + + Assertions.assertThat(runtime) + .isInstanceOf(String.class) + .isEqualTo(expected); + + return myself; + } + + public SELF hasTimeout(final Integer timeoutInSeconds) { + final Map properties = (Map) actual.get("Properties"); + final Integer timeout = (Integer) properties.get("Timeout"); + + Assertions.assertThat(timeout) + .isInstanceOf(Integer.class) + .isEqualTo(timeoutInSeconds); + + return myself; + } + + public SELF hasPolicy(final Map principal, + final String resource, + final String effect, + final String policyDocumentVersion, + final String action, + final Map policyDocument) { + + Assertions.assertThat(policyDocument) + .isNotNull() + .isNotEmpty() + .containsEntry("Version", policyDocumentVersion); + + List> statements = (List>) policyDocument.get("Statement"); + + Assertions.assertThat(statements) + .isNotNull() + .isNotEmpty(); + + Assertions.assertThat(statements).allSatisfy(s -> Assertions.assertThat(s) + .isNotNull() + .isNotEmpty() + .containsEntry("Effect", effect) + .containsEntry("Action", action)); + + Assertions.assertThat(statements) + .anySatisfy(s -> { + if (principal == null) { + return; + } + + Assertions.assertThat(s) + .extracting("Principal") + .asInstanceOf(InstanceOfAssertFactories.MAP) + .matches(e -> e.keySet().containsAll(principal.keySet()) && e.values().containsAll(principal.values())); + }); + + Assertions.assertThat(statements) + .anySatisfy(s -> { + if (resource == null) { + return; + } + + final Map actualResource = (Map) s.get("Resource"); + + if (actualResource.containsKey("Fn::GetAtt")) { + Assertions.assertThat(actualResource) + .isNotEmpty() + .flatExtracting("Fn::GetAtt") + .map(e -> (String) e) + .anySatisfy(e -> Assertions.assertThat(e).matches(resource)); + } else { + Assertions.assertThat(actualResource) + .isNotEmpty() + .extracting("Ref") + .asInstanceOf(InstanceOfAssertFactories.STRING) + .matches(e -> e.matches(resource)); + } + }); + + return myself; + } + + protected String flattenSourceArn(final Map sourceArnMap) { + if (org.apache.commons.collections4.MapUtils.isEmpty(sourceArnMap)) { + return ""; + } + // Get the Fn::Join list from the SourceArn map + final List joinList = (List) sourceArnMap.get("Fn::Join"); + + // Create a StringBuilder to construct the ARN + final StringBuilder arnBuilder = new StringBuilder(); + + // Concatenate the elements of the joinList + for (final Object element : joinList) { + if (element instanceof String) { + arnBuilder.append(element); + } else if (element instanceof List) { + // Handle nested lists + for (final Object nestedElement : (List) element) { + if (nestedElement instanceof String) { + arnBuilder.append(nestedElement); + } else if (nestedElement instanceof Map) { + final Map nestedMap = (Map) nestedElement; + final String refValue = nestedMap.get("Ref"); + if (refValue != null) { + arnBuilder.append(refValue); + } + } + } + } else if (element instanceof Map) { + // Handle nested maps + final Map nestedMap = (Map) element; + final String refValue = nestedMap.get("Ref"); + if (refValue != null) { + arnBuilder.append(refValue); + } + } + } + + // Get the final flattened ARN string + return arnBuilder.toString() + .replace("AWS::Partition", "aws") + .replace("AWS::Region", ""); + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiAccountAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiAccountAssert.java new file mode 100644 index 0000000..1017929 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiAccountAssert.java @@ -0,0 +1,54 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::Account. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsApiAccount(String)}. + */ +@SuppressWarnings("unchecked") +public class ApiAccountAssert extends + AbstractCDKResourcesAssert> { + + private ApiAccountAssert(final Map actual) { + super(actual, ApiAccountAssert.class); + } + + public static ApiAccountAssert assertThat(final Map actual) { + return new ApiAccountAssert(actual); + } + + public ApiAccountAssert hasCloudWatchRole(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map> cloudWatchRoleArn = (Map>) properties.get( + "CloudWatchRoleArn"); + + Assertions.assertThat(cloudWatchRoleArn) + .isNotEmpty() + .flatExtracting("Fn::GetAtt") + .isNotEmpty() + .map(e -> (String) e) + .anySatisfy(arn -> Assertions.assertThat(arn).matches(e -> e.matches(expected))); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiBasePathMappingAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiBasePathMappingAssert.java new file mode 100644 index 0000000..a606333 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiBasePathMappingAssert.java @@ -0,0 +1,73 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::BasePathMapping. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsApiPathMapping(String)}. + */ +@SuppressWarnings("unchecked") +public class ApiBasePathMappingAssert extends AbstractCDKResourcesAssert> { + + private ApiBasePathMappingAssert(final Map actual) { + super(actual, ApiBasePathMappingAssert.class); + } + + public static ApiBasePathMappingAssert assertThat(final Map actual) { + return new ApiBasePathMappingAssert(actual); + } + + public ApiBasePathMappingAssert hasDomainName(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map domainName = (Map) properties.get("DomainName"); + + Assertions.assertThat(domainName) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiBasePathMappingAssert hasRestApiId(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map restApiId = (Map) properties.get("RestApiId"); + + Assertions.assertThat(restApiId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiBasePathMappingAssert hasStage(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map stage = (Map) properties.get("Stage"); + + Assertions.assertThat(stage) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDeploymentAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDeploymentAssert.java new file mode 100644 index 0000000..7c27a6d --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDeploymentAssert.java @@ -0,0 +1,51 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::Deployment. This should be used if the resource map is extracted from the AWS template. Otherwise, start + * with {@link CDKStackAssert#containsApiDeployment(String)}. + */ +@SuppressWarnings("unchecked") +public class ApiDeploymentAssert extends + AbstractCDKResourcesAssert> { + + private ApiDeploymentAssert(final Map actual) { + super(actual, ApiDeploymentAssert.class); + } + + public static ApiDeploymentAssert assertThat(final Map actual) { + return new ApiDeploymentAssert(actual); + } + + public ApiDeploymentAssert hasRestApiId(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map restApiId = (Map) properties.get("RestApiId"); + + Assertions.assertThat(restApiId) + .isNotEmpty() + .extracting("Ref") + .matches(arn -> arn.toString().matches(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDomainNameAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDomainNameAssert.java new file mode 100644 index 0000000..1c1a9a2 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiDomainNameAssert.java @@ -0,0 +1,78 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::DomainName. This should be used if the resource map is extracted from the AWS template. Otherwise, start + * with{@link CDKStackAssert#containsApiDomainName(String)} + */ +@SuppressWarnings("unchecked") +public class ApiDomainNameAssert extends AbstractCDKResourcesAssert> { + + private ApiDomainNameAssert(final Map actual) { + super(actual, ApiDomainNameAssert.class); + } + + public static ApiDomainNameAssert assertThat(final Map actual) { + return new ApiDomainNameAssert(actual); + } + + public ApiDomainNameAssert hasDomainName(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + + final String domainName = (String) properties.get("DomainName"); + + Assertions.assertThat(domainName) + .isNotEmpty() + .isEqualTo(expected); + + return this; + } + + public ApiDomainNameAssert hasEndpointConfiguration(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map endpointConfiguration = (Map) properties.get("EndpointConfiguration"); + final List types = (List) endpointConfiguration.get("Types"); + + Assertions.assertThat(types) + .isNotEmpty() + .contains(expected); + + return this; + } + + public ApiDomainNameAssert hasRegionalCertificateArn(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map regionalCertificateArn = (Map) properties.get("RegionalCertificateArn"); + + Assertions.assertThat(regionalCertificateArn) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiMethodAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiMethodAssert.java new file mode 100644 index 0000000..aa123a1 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiMethodAssert.java @@ -0,0 +1,115 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.apache.commons.lang3.StringUtils; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::Deployment. This should be used if the resource map is extracted from the AWS template. Otherwise, start + * with{@link CDKStackAssert#containsApiMethod(String)} + */ +@SuppressWarnings("unchecked") +public class ApiMethodAssert extends AbstractCDKResourcesAssert> { + + private ApiMethodAssert(final Map actual) { + super(actual, ApiMethodAssert.class); + } + + public static ApiMethodAssert assertThat(final Map actual) { + return new ApiMethodAssert(actual); + } + + public ApiMethodAssert hasRestApiId(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map restApiId = (Map) properties.get("RestApiId"); + + Assertions.assertThat(restApiId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiMethodAssert hasAuthorizationType(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String authorizationType = properties.get("AuthorizationType"); + + Assertions.assertThat(authorizationType) + .isNotBlank() + .matches(e -> e.matches(expected)); + + return this; + } + + public ApiMethodAssert hasHttpMethod(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String httpMethod = properties.get("HttpMethod"); + + Assertions.assertThat(httpMethod) + .isNotBlank() + .matches(e -> e.matches(expected)); + + return this; + } + + public ApiMethodAssert hasIntegration(final String method, final String type, final String uri) { + final Map properties = (Map) actual.get("Properties"); + final Map integration = (Map) properties.get("Integration"); + final Map actual = (Map) integration.get("Uri"); + + final String actualUrl = flattenSourceArn(actual); + + Assertions.assertThat(integration) + .isNotEmpty() + .containsEntry("IntegrationHttpMethod", method) + .containsEntry("Type", type); + + if (StringUtils.isNotBlank(actualUrl)) { + Assertions.assertThat(integration) + .containsKey("Uri"); + } + + return this; + } + + public ApiMethodAssert hasResourceId(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map resourceId = (Map) properties.get("ResourceId"); + + if(resourceId.containsKey("Fn::GetAtt")) { + Assertions.assertThat(resourceId) + .isNotEmpty() + .flatExtracting("Fn::GetAtt") + .map(e -> (String) e) + .anySatisfy(e -> Assertions.assertThat(e).matches(expected)) + .anySatisfy(e -> Assertions.assertThat(e).matches("RootResourceId")); + } else { + Assertions.assertThat(resourceId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + } + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiResourceAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiResourceAssert.java new file mode 100644 index 0000000..035a494 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiResourceAssert.java @@ -0,0 +1,83 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::Resource. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsApiResource(String)}. + */ +@SuppressWarnings("unchecked") +public class ApiResourceAssert extends + AbstractCDKResourcesAssert> { + + private ApiResourceAssert(final Map actual) { + super(actual, ApiResourceAssert.class); + } + + public static ApiResourceAssert assertThat(final Map actual) { + return new ApiResourceAssert(actual); + } + + public ApiResourceAssert hasRestApiId(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map restApiId = (Map) properties.get("RestApiId"); + + Assertions.assertThat(restApiId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiResourceAssert hasPath(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String pathPart = properties.get("PathPart"); + + Assertions.assertThat(pathPart) + .isNotBlank() + .matches(sn -> sn.equals(expected)); + + return this; + } + + public ApiResourceAssert hasParentId(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map> parentId = (Map>) properties.get("ParentId"); + + if (parentId.containsKey("Fn::GetAtt")) { + Assertions.assertThat(parentId) + .isNotEmpty() + .flatExtracting("Fn::GetAtt") + .map(e -> (String) e) + .anySatisfy(e -> Assertions.assertThat(e).matches(expected)) + .anySatisfy(e -> Assertions.assertThat(e).matches("RootResourceId")); + } else { + Assertions.assertThat(parentId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + } + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiStageAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiStageAssert.java new file mode 100644 index 0000000..248c987 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/ApiStageAssert.java @@ -0,0 +1,73 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::Stage. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsApiStage(String)}. + */ +@SuppressWarnings("unchecked") +public class ApiStageAssert extends + AbstractCDKResourcesAssert> { + + private ApiStageAssert(final Map actual) { + super(actual, ApiStageAssert.class); + } + + public static ApiStageAssert assertThat(final Map actual) { + return new ApiStageAssert(actual); + } + + public ApiStageAssert hasRestApiId(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map restApiId = (Map) properties.get("RestApiId"); + + Assertions.assertThat(restApiId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiStageAssert hasDeploymentId(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map deploymentId = (Map) properties.get("DeploymentId"); + + Assertions.assertThat(deploymentId) + .isNotEmpty() + .extracting("Ref") + .matches(e -> e.toString().matches(expected)); + + return this; + } + + public ApiStageAssert hasStageName(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String stageName = properties.get("StageName"); + + Assertions.assertThat(stageName) + .isNotBlank() + .matches(sn -> sn.matches(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CDKStackAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CDKStackAssert.java new file mode 100644 index 0000000..ee458c1 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CDKStackAssert.java @@ -0,0 +1,473 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import static software.amazon.awscdk.assertions.Match.stringLikeRegexp; + +import java.util.Map; +import java.util.Map.Entry; +import org.assertj.core.api.AbstractAssert; +import org.assertj.core.api.Assertions; +import software.amazon.awscdk.assertions.Template; + +@SuppressWarnings("unchecked") +public class CDKStackAssert extends AbstractAssert { + + private CDKStackAssert(final Template actual) { + super(actual, CDKStackAssert.class); + } + + /** + * Fluent assertions for CDK resources. Assertions are done directly on an object of {@link software.amazon.awscdk.assertions.Template}. + * + * @param actual {@link Template} instance + * @return {@link CDKStackAssert} instance + */ + public static CDKStackAssert assertThat(final Template actual) { + return new CDKStackAssert(actual); + } + + /** + * Fluent assertions for AWS::ApiGateway::RestApi. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link RestApiAssert} should be used instead. + *

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id rest api id + * @return {@link RestApiAssert} instance + */ + public RestApiAssert containsRestApi(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_RESTAPI, id); + + return RestApiAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::Account. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiAccountAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id of the rest api account + * @return {@link ApiAccountAssert} instance + */ + public ApiAccountAssert containsApiAccount(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_ACCOUNT, id); + + return ApiAccountAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::Deployment. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiDeploymentAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id Rest API deployement id + * @return {@link ApiDeploymentAssert} instance + */ + public ApiDeploymentAssert containsApiDeployment(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_DEPLOYMENT, id); + + return ApiDeploymentAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::Method. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiMethodAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id Rest API method id + * @return {@link ApiMethodAssert} instance + */ + public ApiMethodAssert containsApiMethod(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_METHOD, id); + + return ApiMethodAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::Stage. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiStageAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id Rest API stage id + * @return {@link ApiStageAssert} instance + */ + public ApiStageAssert containsApiStage(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_STAGE, id); + + return ApiStageAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::Resource. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiResourceAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id Rest API resource id + */ + public ApiResourceAssert containsApiResource(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_RESOURCE, id); + + return ApiResourceAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::IAM::Policy. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link PolicyAssert} should be used instead. + *

+ *

+ * Example usage can be found in sandpipers-cdk-example-lambda/test + *

+ * + * @param id policy id + * @return {@link PolicyAssert} instance + */ + public PolicyAssert containsPolicy(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.POLICY, id); + + return PolicyAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::IAM::Role. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link RoleAssert} should be used instead. + *

+ *

+ * Example usage can be found in sandpipers-cdk-example-lambda/test + *

+ * + * @param id managed policy arn + * @return {@link RoleAssert} instance + */ + public RoleAssert containsRole(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.ROLE, id); + + return RoleAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::Lambda::Function. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link LambdaAssert} should be used instead. + *

+ *

+ * Example usage can be found in sandpipers-cdk-example-lambda/test + *

+ * + * @param id expected function id + * @return {@link LambdaAssert} instance + */ + public LambdaAssert containsFunction(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.LAMBDA_FUNCTION, id); + + return LambdaAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::Lambda::EventInvokeConfig. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link LambdaEventInvokeConfigAssert} should + * be used instead. + * + *

+ * Example usage can be found in sandpipers-cdk-example-lambda/test + *

+ * + * @param id the id of the lambda event invoke config + * @return {@link LambdaEventInvokeConfigAssert} instance + */ + public LambdaEventInvokeConfigAssert containsLambdaEventInvokeConfig(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.LAMBDA_EVENT_INVOKE_CONFIG, id); + + return LambdaEventInvokeConfigAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::Permission::Permission. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link LambdaPermissionAssert} should be used + * instead.

+ *

+ * Example usages can be found in: + *

+ *

+ * + * @param id the name of the lambda function + * @return {@link LambdaPermissionAssert} instance + */ + public LambdaPermissionAssert containsLambdaPermission(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.LAMBDA_PERMISSION, id); + + return LambdaPermissionAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::BasePathMapping. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiDomainNameAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id the name of the lambda function + * @return {@link ApiDomainNameAssert} instance + */ + public ApiDomainNameAssert containsApiDomainName(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_DOMAIN_NAME, id); + + return ApiDomainNameAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::ApiGateway::BasePathMapping. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link ApiBasePathMappingAssert} should be + * used instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id the name of the lambda function + * @return {@link ApiBasePathMappingAssert} instance + */ + public ApiBasePathMappingAssert containsApiPathMapping(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.APIGATEWAY_BASE_PATH_MAPPING, id); + + return ApiBasePathMappingAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::SQS::QueuePolicy. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link QueuePolicyAssert} should be used + * instead. + * + *

+ * Example: + *

+ *
+   *   {@code
+   *       CDKStackAssert.assertThat(template)
+   *         .containsQueuePolicy("examplelambdafunctionfailurequeue(.*)", "sqs:SendMessage", "examplelambdafunctionfailurequeue(.*)", "sns.amazonaws.com")
+   *         .hasEffect("Allow")
+   *         .hasCondition("ArnEquals", "aws:SourceArn", "examplelambdafunctionfailuretopic(.*)");
+   *     }
+   * 
+ * + * @param id the queue policy id + * @return {@link QueuePolicyAssert} instance + */ + public QueuePolicyAssert containsQueuePolicy(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.QUEUE_POLICY, id); + + return QueuePolicyAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::SQS::Queue. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link QueueAssert} should be used instead. + * + *

+ * Example usage can be found in sandpipers-cdk-example-sqs/test + *

+ * + * @param id the key of the queue + * @return {@link QueueAssert} instance + */ + public QueueAssert containsQueue(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.QUEUE, id); + + return QueueAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::SNS::Topic. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link TopicAssert} should be used instead. + * + *

+ * Example usage can be found in sandpipers-cdk-example-sns/test + *

+ * + * @param id the key of the queue + * @return {@link TopicAssert} instance + */ + public TopicAssert containsTopic(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.TOPIC, id); + + return TopicAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::SNS::Subscription. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link TopicSubscriptionAssert} should be + * used instead. + * + *

+ * Example usage can be found in sandpipers-cdk-example-sns/test + *

+ * + * @param id the topic subscription id + * @return {@link TopicSubscriptionAssert} instance + */ + public TopicSubscriptionAssert containsTopicSubscription(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.TOPIC_SUBSCRIPTION, id); + + return TopicSubscriptionAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::CertificateManager::Certificate. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link CertificateAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-apigateway/test + *

+ * + * @param id the name of the lambda function + * @return {@link CertificateAssert} instance + */ + public CertificateAssert containsCertificate(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.CERTIFICATEMANAGER_CERTIFICATE, id); + + return CertificateAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::Route53::ARecord. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link RecordSetAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-route53/test + *

+ * + * @param id the name of the lambda function + * @return {@link RecordSetAssert} instance + */ + public RecordSetAssert containsRecordSet(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.ROUTE53_RECORD_SET, id); + + return RecordSetAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::Route53::ARecord. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link HostedZoneAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-route53/test + *

+ * + * @param id the name of the lambda function + * @return {@link HostedZoneAssert} instance + */ + public HostedZoneAssert containsHostedZone(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.ROUTE53_HOSTED_ZONE, id); + + return HostedZoneAssert.assertThat(resource.getValue()); + } + + /** + * Fluent assertions for AWS::DynamoDB::GlobalTable. Assertions are done directly on an object of + * {@link software.amazon.awscdk.assertions.Template}. If a resource map has been extracted from, then {@link DynamoDBGlobalTableAssert} should be used + * instead.

+ *

+ * Example usages can be found in sandpipers-cdk-example-dynamodb/test + *

+ * + * @param id the name of the lambda function + * @return {@link DynamoDBGlobalTableAssert} instance + */ + public DynamoDBGlobalTableAssert containsDynamoDBTable(final String id) { + + final Entry> resource = containsResource(actual, CdkResourceType.DYNAMODB_GLOBAL_TABLE, id); + + return DynamoDBGlobalTableAssert.assertThat(resource.getValue()); + } + + private Entry> containsResource( + final Template template, + final CdkResourceType cdkResourceType, + final String id) { + + final Entry> resource = template.findResources(cdkResourceType.getValue()) + .entrySet() + .stream() + .filter(entry -> stringLikeRegexp(id).test(entry.getKey()).getIsSuccess()) + .findFirst() + .orElseThrow(); + + Assertions.assertThat(resource) + .isNotNull(); + + return resource; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java new file mode 100644 index 0000000..c3961bb --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CdkResourceType.java @@ -0,0 +1,54 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public enum CdkResourceType { + APIGATEWAY_RESTAPI("AWS::ApiGateway::RestApi"), + APIGATEWAY_METHOD("AWS::ApiGateway::Method"), + APIGATEWAY_RESOURCE("AWS::ApiGateway::Resource"), + APIGATEWAY_ACCOUNT("AWS::ApiGateway::Account"), + APIGATEWAY_DEPLOYMENT("AWS::ApiGateway::Deployment"), + APIGATEWAY_STAGE("AWS::ApiGateway::Stage"), + APIGATEWAY_DOMAIN_NAME("AWS::ApiGateway::DomainName"), + APIGATEWAY_BASE_PATH_MAPPING("AWS::ApiGateway::BasePathMapping"), + LAMBDA_FUNCTION("AWS::Lambda::Function"), + LAMBDA_EVENT_INVOKE_CONFIG("AWS::Lambda::EventInvokeConfig"), + LAMBDA_PERMISSION("AWS::Lambda::Permission"), + POLICY("AWS::IAM::Policy"), + ROLE("AWS::IAM::Role"), + TOPIC("AWS::SNS::Topic"), + TOPIC_SUBSCRIPTION("AWS::SNS::Subscription"), + QUEUE("AWS::SQS::Queue"), + QUEUE_POLICY("AWS::SQS::QueuePolicy"), + BUCKET("AWS::S3::Bucket"), + ECS_TASK_DEFINITION("AWS::ECS::TaskDefinition"), + DYNAMODB_TABLE("AWS::DynamoDB::Table"), + DYNAMODB_GLOBAL_TABLE("AWS::DynamoDB::GlobalTable"), + CERTIFICATEMANAGER_CERTIFICATE("AWS::CertificateManager::Certificate"), + ROUTE53_RECORD_SET("AWS::Route53::RecordSet"), + ROUTE53_HOSTED_ZONE("AWS::Route53::HostedZone"), + ; + + private String value; +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java new file mode 100644 index 0000000..d3702f8 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/CertificateAssert.java @@ -0,0 +1,88 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::CertificateManager::Certificate. This should be used if the resource map is extracted from the AWS template. Otherwise, start + * with{@link CDKStackAssert#containsCertificate(String)} + */ +@SuppressWarnings("unchecked") +public class CertificateAssert extends AbstractCDKResourcesAssert> { + + private CertificateAssert(final Map actual) { + super(actual, CertificateAssert.class); + } + + public static CertificateAssert assertThat(final Map actual) { + return new CertificateAssert(actual); + } + + public CertificateAssert hasDomainName(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + + final String domainName = (String) properties.get("DomainName"); + + Assertions.assertThat(domainName) + .isNotEmpty() + .isEqualTo(expected); + + return this; + } + + public CertificateAssert hasDomainValidationOptions(final String domainName, final String validationDomain) { + + final Map properties = (Map) actual.get("Properties"); + final List< Object> domainValidationOptions = (List< Object>) properties.get("DomainValidationOptions"); + + Assertions.assertThat(domainValidationOptions) + .isNotNull() + .isNotEmpty() + .anySatisfy(dvo -> { + final Map domainValidationOption = (Map) dvo; + final String domainNameValue = (String) domainValidationOption.get("DomainName"); + final String validationDomainValue = (String) domainValidationOption.get("ValidationDomain"); + + Assertions.assertThat(domainNameValue) + .isNotEmpty() + .isEqualTo(domainName); + + Assertions.assertThat(validationDomainValue) + .isNotEmpty() + .isEqualTo(validationDomain); + }); + + return this; + } + + public CertificateAssert hasValidationMethod(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String validationMethod = (String) properties.get("ValidationMethod"); + + Assertions.assertThat(validationMethod) + .isNotEmpty() + .isEqualTo(expected); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java new file mode 100644 index 0000000..6f86ca5 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/DynamoDBGlobalTableAssert.java @@ -0,0 +1,117 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::DynamoDB::GlobalTable. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsDynamoDBTable(String)}. + */ +@SuppressWarnings("unchecked") +public class DynamoDBGlobalTableAssert extends + AbstractCDKResourcesAssert> { + + private DynamoDBGlobalTableAssert(final Map actual) { + super(actual, DynamoDBGlobalTableAssert.class); + } + + public static DynamoDBGlobalTableAssert assertThat(final Map actual) { + return new DynamoDBGlobalTableAssert(actual); + } + + public DynamoDBGlobalTableAssert hasBillingMode(final String expected) { + + final String billingMode = ((Map) actual.get("Properties")).get("BillingMode"); + + Assertions.assertThat(billingMode) + .isNotNull() + .isInstanceOf(String.class) + .matches(bm -> bm.matches(expected)); + + return this; + } + + public DynamoDBGlobalTableAssert hasName(final String expected) { + + final String billingMode = ((Map) actual.get("Properties")).get("TableName"); + + Assertions.assertThat(billingMode) + .isNotNull() + .isInstanceOf(String.class) + .matches(bm -> bm.matches(expected)); + + return this; + } + + public DynamoDBGlobalTableAssert hasSSESpecification(final Boolean expected) { + + final Map properties = ((Map) actual.get("Properties")); + final Map sseSpecification = ((Map) properties.get("SSESpecification")); + final Boolean sseEnabled = sseSpecification.get("SSEEnabled"); + + Assertions.assertThat(sseEnabled) + .isEqualTo(expected); + + return this; + } + + public DynamoDBGlobalTableAssert hasKeySchema(final String name, final String type) { + + final Map properties = ((Map) actual.get("Properties")); + final List> keySchema = ((List>) properties.get("KeySchema")); + + Assertions.assertThat(keySchema) + .isInstanceOf(List.class) + .anySatisfy(item -> { + Assertions.assertThat(item.get("AttributeName")) + .isNotNull() + .isInstanceOf(String.class) + .isEqualTo(name); + + Assertions.assertThat(item.get("KeyType")) + .isNotNull() + .isInstanceOf(String.class) + .isEqualTo(type); + }); + return this; + } + + public DynamoDBGlobalTableAssert hasAttributeDefinitions(final String name, final String type) { + + final Map properties = ((Map) actual.get("Properties")); + final List> attributeDefinitions = ((List>) properties.get("AttributeDefinitions")); + + Assertions.assertThat(attributeDefinitions) + .isInstanceOf(List.class) + .anySatisfy(item -> { + Assertions.assertThat(item.get("AttributeName")) + .isNotNull() + .isInstanceOf(String.class) + .isEqualTo(name); + + Assertions.assertThat(item.get("AttributeType")) + .isNotNull() + .isInstanceOf(String.class) + .isEqualTo(type); + }); + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/HostedZoneAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/HostedZoneAssert.java new file mode 100644 index 0000000..fc2d091 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/HostedZoneAssert.java @@ -0,0 +1,55 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::Route53::HostedZone. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + *
    + *
  • {@link CDKStackAssert#containsHostedZone(String)}
  • + *
+ */ +@SuppressWarnings("unchecked") +public class HostedZoneAssert extends AbstractCDKResourcesAssert> { + + private HostedZoneAssert(final Map actual) { + super(actual, HostedZoneAssert.class); + } + + public static HostedZoneAssert assertThat(final Map actual) { + return new HostedZoneAssert(actual); + } + + public HostedZoneAssert hasName(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String name = (String) properties.get("Name"); + + Assertions.assertThat(name) + .isNotBlank() + .matches(expected); + + return this; + } + + public HostedZoneAssert hasTag(final String key, final Object value) { + hasTag("HostedZoneTags", key, value); + return myself; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaAssert.java new file mode 100644 index 0000000..f5aece8 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaAssert.java @@ -0,0 +1,131 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::Lambda::Function. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsFunction(String)}. + */ +@SuppressWarnings("unchecked") +public class LambdaAssert extends AbstractCDKResourcesAssert> { + + private LambdaAssert(final Map actual) { + super(actual, LambdaAssert.class); + } + + public static LambdaAssert assertThat(final Map actual) { + return new LambdaAssert(actual); + } + + public LambdaAssert hasFunction(final String expected) { + final Map properties = (Map) actual.get("Properties"); + + Assertions.assertThat(properties.get("FunctionName")) + .isInstanceOf(String.class) + .matches(expected::equals); + + return this; + } + + public LambdaAssert hasHandler(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String handler = (String) properties.get("Handler"); + + Assertions.assertThat(handler) + .isInstanceOf(String.class) + .isEqualTo(expected); + + return this; + } + + public LambdaAssert hasCode(final String s3Bucket, final String s3Key) { + final Map properties = (Map) actual.get("Properties"); + final Map lambdaCode = (Map) properties.get("Code"); + + String actualS3Bucket = ((Map) lambdaCode.get("S3Bucket")).get("Fn::Sub"); + String actualS3Key = (String) lambdaCode.get("S3Key"); + + Assertions.assertThat(actualS3Bucket) + .matches(s3Bucket); + + Assertions.assertThat(actualS3Key) + .matches(s3Key); + + return this; + } + + public LambdaAssert hasRetryAttempts(final Integer expected) { + final Map properties = (Map) actual.get("Properties"); + final Integer retryAttempts = (Integer) properties.get("retryAttempts"); + + Assertions.assertThat(retryAttempts) + .isInstanceOf(Integer.class) + .isEqualTo(expected); + + return this; + } + + public LambdaAssert hasMaxAgeEvent(final Integer expected) { + final Map properties = (Map) actual.get("Properties"); + final Integer memorySize = (Integer) properties.get("maxAgeEvent"); + + Assertions.assertThat(memorySize) + .isInstanceOf(Integer.class) + .isEqualTo(expected); + + return this; + } + + public LambdaAssert hasRole(final String arnRegex) { + final Map properties = (Map) actual.get("Properties"); + final Map role = (Map) properties.get("Role"); + final List roleArnFun = (List) role.get("Fn::GetAtt"); + + Assertions.assertThat(roleArnFun) + .isInstanceOf(List.class) + .anySatisfy(s -> Assertions.assertThat(s) + .isInstanceOf(String.class) + .matches(e -> e.matches(arnRegex))); + + return this; + } + + public LambdaAssert hasDeadLetterTarget(final String targetArn) { + final Map properties = (Map) actual.get("Properties"); + final Map role = (Map) properties.get("DeadLetterConfig"); + final Map actualTargetArn = (Map) role.get("TargetArn"); + + if(actualTargetArn.containsKey("Fn::GetAtt")) { + final List list = (List) actualTargetArn.get("Fn::GetAtt"); + Assertions.assertThat(list) + .isNotEmpty() + .anySatisfy(e -> Assertions.assertThat(e).matches(targetArn)); + } else { + Assertions.assertThat(actualTargetArn) + .isInstanceOf(Map.class) + .anySatisfy((key, value) -> Assertions.assertThat(key) + .matches(e -> e.matches("Ref") && String.valueOf(value).matches(targetArn))); + } + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaEventInvokeConfigAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaEventInvokeConfigAssert.java new file mode 100644 index 0000000..a82c101 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaEventInvokeConfigAssert.java @@ -0,0 +1,127 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::Lambda::EventInvokeConfig. This should be used if the resource map is extracted from the AWS template. Otherwise, start + * with {@link CDKStackAssert#containsLambdaEventInvokeConfig(String)}. + */ +@SuppressWarnings("unchecked") +public class LambdaEventInvokeConfigAssert extends + AbstractCDKResourcesAssert> { + + private LambdaEventInvokeConfigAssert(final Map actual) { + super(actual, LambdaEventInvokeConfigAssert.class); + } + + public static LambdaEventInvokeConfigAssert assertThat(final Map actual) { + return new LambdaEventInvokeConfigAssert(actual); + } + + public LambdaEventInvokeConfigAssert hasLambdaEventInvokeConfig(final String expectedFunctionName, + final String expectedSuccessEventDestination, + final String expectedFailureEventDestination) { + final Map properties = (Map) actual.get("Properties"); + + final String actualFunctionName = ((Map) properties.get("FunctionName")).get("Ref"); + + Assertions.assertThat(actualFunctionName) + .isInstanceOf(String.class) + .matches(actual -> actual.matches(expectedFunctionName)); + + final Map destinationConfig = (Map) properties.get("DestinationConfig"); + + if (isNotBlank(expectedSuccessEventDestination)) { + final String actualSuccessDestinationConfig = getEventSuccessDestination(destinationConfig); + + Assertions.assertThat(actualSuccessDestinationConfig) + .isInstanceOf(String.class) + .matches(expectedSuccessEventDestination); + } + + if (isNotBlank(expectedFailureEventDestination)) { + final String actualFailureDestinationConfig = getEventFailureDestination(destinationConfig); + Assertions.assertThat(actualFailureDestinationConfig) + .isInstanceOf(String.class) + .matches(expectedFailureEventDestination); + } + + return this; + } + + public LambdaEventInvokeConfigAssert hasMaximumRetryAttempts(final Integer expected) { + final Map properties = (Map) actual.get("Properties"); + + Assertions.assertThat((Integer) properties.get("MaximumRetryAttempts")) + .isInstanceOf(Integer.class) + .isEqualTo(expected); + + return this; + } + + public LambdaEventInvokeConfigAssert hasMaximumEventAgeInSeconds(final Integer expected) { + final Map properties = (Map) actual.get("Properties"); + Assertions.assertThat((Integer) properties.get("MaximumEventAgeInSeconds")) + .isInstanceOf(Integer.class) + .isEqualTo(expected); + + return this; + } + + public LambdaEventInvokeConfigAssert hasQualifier(final String expected) { + final Map properties = (Map) actual.get("Properties"); + + Assertions.assertThat((String) properties.get("Qualifier")) + .isInstanceOf(String.class) + .matches(actual -> actual.equals(expected)); + + return this; + } + public LambdaEventInvokeConfigAssert hasFunctionName(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map functionName = (Map) properties.get("FunctionName"); + final String functionReference = functionName.get("Ref"); + + Assertions.assertThat(functionReference) + .matches(actual -> actual.matches(expected)); + + return this; + } + + private String getEventFailureDestination(final Map destinationConfig) { + return getLambdaEventDestination(destinationConfig, "OnFailure"); + } + + private String getEventSuccessDestination(final Map destinationConfig) { + return getLambdaEventDestination(destinationConfig, "OnSuccess"); + } + + private String getLambdaEventDestination(final Map destinationConfig, + final String successFailureKey) { + final Map successFailureDestinationConfig = + (Map) destinationConfig.get(successFailureKey); + final Map destination = + (Map) successFailureDestinationConfig.get("Destination"); + return destination.get("Ref"); + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaPermissionAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaPermissionAssert.java new file mode 100644 index 0000000..6abf711 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/LambdaPermissionAssert.java @@ -0,0 +1,81 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import static org.apache.commons.lang3.StringUtils.isNotBlank; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.MapAssert; + +/** + * Fluent assertions for AWS::Lambda::Permission. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsLambdaPermission(String)}. + */ +@SuppressWarnings("unchecked") +public class LambdaPermissionAssert extends AbstractCDKResourcesAssert> { + + private LambdaPermissionAssert(final Map actual) { + super(actual, LambdaPermissionAssert.class); + } + + public static LambdaPermissionAssert assertThat(final Map actual) { + return new LambdaPermissionAssert(actual); + } + + public LambdaPermissionAssert hasLambdaPermission( + final String functionName, + final String action, + final String principal, + final String expectedSourceArn) { + + final Map properties = (Map) actual.get("Properties"); + + final MapAssert mapAssert = Assertions.assertThat(properties) + .isNotNull() + .isNotEmpty() + .containsEntry("Action", action); + + mapAssert.satisfies(s -> { + final List functionNameList = (List) ((Map) s.get("FunctionName")).get("Fn::GetAtt"); + + Assertions.assertThat(functionNameList) + .isNotNull() + .hasAtLeastOneElementOfType(String.class) + .anySatisfy(e -> Assertions.assertThat((String) e) + .matches(func -> func.matches(functionName))); + }); + + if (isNotBlank(principal)) { + mapAssert.satisfies(s -> Assertions.assertThat(s) + .extracting("Principal") + .matches(e -> e.toString().matches(principal))); + } + + final Map sourceArn = (Map) properties.get("SourceArn"); + final String actualSourceArn = flattenSourceArn(sourceArn); + + Assertions.assertThat(actualSourceArn) + .isNotNull() + .isInstanceOf(String.class) + .matches(actual -> actual.matches(expectedSourceArn)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/PolicyAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/PolicyAssert.java new file mode 100644 index 0000000..5c867d5 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/PolicyAssert.java @@ -0,0 +1,76 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::IAM::Policy. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsPolicy(String)}. + */ +@SuppressWarnings("unchecked") +public class PolicyAssert extends + AbstractCDKResourcesAssert> { + + private PolicyAssert(final Map actual) { + super(actual, PolicyAssert.class); + } + + public static PolicyAssert assertThat(final Map actual) { + return new PolicyAssert(actual); + } + + public PolicyAssert hasName(final String expected) { + + final String policyName = ((Map) actual.get("Properties")).get("PolicyName"); + + Assertions.assertThat(policyName) + .isNotNull() + .isInstanceOf(String.class) + .matches(actualPolicyName -> actualPolicyName.matches(expected)); + + return this; + } + + public PolicyAssert hasPolicyDocumentStatement(final Map principal, + final String resource, + final String action, + final String effect, + final String policyDocumentVersion) { + final Map properties = + ((Map) actual.get("Properties")); + + final Map policyDocument = + ((Map) properties.get("PolicyDocument")); + + return hasPolicy(principal, resource, effect, policyDocumentVersion, action, policyDocument); + } + + public PolicyAssert isAssociatedWithRole(final String expected) { + final List roles = ((Map>) actual.get("Properties")).get("Roles"); + + Assertions.assertThat(roles) + .isNotNull() + .isNotEmpty() + .allMatch(role -> ((Map) role).get("Ref").matches(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueueAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueueAssert.java new file mode 100644 index 0000000..dcc00c7 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueueAssert.java @@ -0,0 +1,123 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; +import software.amazon.awscdk.services.sqs.RedriveAllowPolicy; + +/** + * Fluent assertions for AWS::SQS::Queue. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsQueue(String)}. + */ +@SuppressWarnings("unchecked") +public class QueueAssert extends AbstractCDKResourcesAssert> { + + private QueueAssert(final Map actual) { + super(actual, QueueAssert.class); + } + + public static QueueAssert assertThat(final Map actual) { + return new QueueAssert(actual); + } + + public QueueAssert hasDeadLetterQueue(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map> redrivePolicy = (Map>) properties.get("RedrivePolicy"); + final Map deadLetterTargetArn = (Map) redrivePolicy.get("deadLetterTargetArn"); + final List roleArnFun = (List) deadLetterTargetArn.get("Fn::GetAtt"); + + Assertions.assertThat(roleArnFun) + .isInstanceOf(List.class) + .anySatisfy(s -> Assertions.assertThat(s) + .isInstanceOf(String.class) + .matches(e -> e.matches(expected))); + + return this; + } + + public QueueAssert isFifo(final boolean expected) { + + final Map properties = (Map) actual.get("Properties"); + final Boolean isFifo = (Boolean) properties.get("FifoQueue"); + + Assertions.assertThat(isFifo) + .isEqualTo(expected); + + return this; + } + + public QueueAssert hasRedriveAllowPolicy(final RedriveAllowPolicy expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map redriveAllowPolicy = (Map) properties.get("RedriveAllowPolicy"); + + Assertions.assertThat(redriveAllowPolicy) + .isEqualTo(new ObjectMapper().convertValue(expected, Map.class)); + + return this; + } + + public QueueAssert hasContentBasedDeduplicationEnabled(final boolean expected) { + + final Map properties = (Map) actual.get("Properties"); + final Boolean isFifo = properties.get("ContentBasedDeduplication"); + + Assertions.assertThat(isFifo) + .isEqualTo(expected); + + return this; + } + + public QueueAssert hasDeduplicationScope(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String deduplicationScope = properties.get("DeduplicationScope"); + + Assertions.assertThat(deduplicationScope) + .isEqualTo(expected); + + return this; + } + + public QueueAssert hasFifoThroughputLimit(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String fifoThroughputLimit = properties.get("FifoThroughputLimit"); + + Assertions.assertThat(fifoThroughputLimit) + .isEqualTo(expected); + + return this; + } + + public QueueAssert hasMaxRetrialCount(final Integer deletionPolicy) { + + final Map properties = (Map) actual.get("Properties"); + final Map redrivePolicy = (Map) properties.get("RedrivePolicy"); + final Integer maxReceiveCount = redrivePolicy.get("maxReceiveCount"); + + Assertions.assertThat(maxReceiveCount) + .isEqualTo(deletionPolicy); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueuePolicyAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueuePolicyAssert.java new file mode 100644 index 0000000..a1e6a05 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/QueuePolicyAssert.java @@ -0,0 +1,148 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import static org.apache.commons.lang3.StringUtils.isNoneBlank; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::SQS::QueuePolicy. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsQueuePolicy(String)}. + */ +@SuppressWarnings("unchecked") +public class QueuePolicyAssert extends AbstractCDKResourcesAssert> { + + private QueuePolicyAssert(final Map actual) { + super(actual, QueuePolicyAssert.class); + } + + public static QueuePolicyAssert assertThat(final Map actual) { + return new QueuePolicyAssert(actual); + } + + public QueuePolicyAssert hasQueuePolicy( + final String queueReference, + final String policyStatementAction, + final String policyStatementResource, + final String policyStatementPrincipalService + ) { + + final Map properties = (Map) actual.get("Properties"); + + final List> queues = (List>) properties.get("Queues"); + final Map policyDocument = + (Map) properties.get("PolicyDocument"); + final List> statements = + (List>) policyDocument.get("Statement"); + + Assertions.assertThat(queues) + .isNotNull() + .isNotEmpty() + .hasSize(1) + .extracting("Ref") + .element(0) + .matches(e -> ((String) e).matches(queueReference)); + + boolean actionFound = false; + boolean principalServiceFound = false; + boolean resourceFound = false; + for (Map statement : statements) { + actionFound = statement.get("Action").equals(policyStatementAction); + + final Map principal = (Map) statement.get("Principal"); + final String service = (String) principal.get("Service"); + principalServiceFound = + isNoneBlank(service) && service.equals(policyStatementPrincipalService); + + final List resources = ((Map>) statement.get("Resource")).get( + "Fn::GetAtt"); + + final String resource = (String) resources.get(0); + resourceFound = isNoneBlank(resource) && resource.matches(policyStatementResource); + } + + Assertions.assertThat(policyDocument.get("Version")) + .isInstanceOf(String.class) + .isEqualTo("2012-10-17"); + + Assertions.assertThat(actionFound) + .isTrue(); + + Assertions.assertThat(principalServiceFound) + .isTrue(); + + Assertions.assertThat(resourceFound) + .isTrue(); + + return this; + } + + public QueuePolicyAssert hasEffect(final String effect) { + final Map properties = (Map) actual.get("Properties"); + + final Map policyDocument = + (Map) properties.get("PolicyDocument"); + final List> statements = + (List>) policyDocument.get("Statement"); + + boolean effectFound = false; + for (Map statement : statements) { + effectFound = statement.get("Effect").equals(effect); + } + + Assertions.assertThat(effectFound) + .isTrue(); + + return this; + } + + public QueuePolicyAssert hasCondition( + final String policyStatementConditionType, + final String policyStatementConditionSourceType, + final String policyStatementConditionSourceArn + ) { + final Map properties = (Map) actual.get("Properties"); + + final Map policyDocument = + (Map) properties.get("PolicyDocument"); + final List> statements = + (List>) policyDocument.get("Statement"); + + boolean conditionMatch = false; + for (Map statement : statements) { + final Map condition = (Map) statement.get("Condition"); + final Map conditionType = + (Map) condition.get(policyStatementConditionType); + + final Map conditionSourceType = + (Map) conditionType.get(policyStatementConditionSourceType); + + conditionMatch = + ((String) conditionSourceType.get("Ref")).matches(policyStatementConditionSourceArn); + } + + Assertions.assertThat(conditionMatch) + .isTrue(); + + return this; + } + +} \ No newline at end of file diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RecordSetAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RecordSetAssert.java new file mode 100644 index 0000000..a565f7a --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RecordSetAssert.java @@ -0,0 +1,97 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::Route53::RecordSet. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + *
    + *
  • {@link CDKStackAssert#containsRecordSet(String)}
  • + *
+ */ +@SuppressWarnings("unchecked") +public class RecordSetAssert extends AbstractCDKResourcesAssert> { + + private RecordSetAssert(final Map actual) { + super(actual, RecordSetAssert.class); + } + + public static RecordSetAssert assertThat(final Map actual) { + return new RecordSetAssert(actual); + } + + public RecordSetAssert hasName(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String name = (String) properties.get("Name"); + + Assertions.assertThat(name) + .isNotBlank() + .matches(expected); + + return this; + } + + public RecordSetAssert hasType(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String type = (String) properties.get("Type"); + + Assertions.assertThat(type) + .isNotBlank() + .matches(expected); + + return this; + } + + public RecordSetAssert hasTtl(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String ttl = (String) properties.get("TTL"); + + Assertions.assertThat(ttl) + .isNotNull() + .isEqualTo(expected); + + return this; + } + + public RecordSetAssert hasHostedZone(final String id) { + final Map> properties = ((Map>) actual.get("Properties")); + final Map hostedZoneId = (Map)properties.get("HostedZoneId"); + + Assertions.assertThat(hostedZoneId) + .isNotNull() + .isNotEmpty() + .anySatisfy((k,v) -> Assertions.assertThat(v).matches(id)); + + return this; + } + + public RecordSetAssert hasResourceRecords(final List expected) { + final Map properties = (Map) actual.get("Properties"); + final List resourceRecords = (List) properties.get("ResourceRecords"); + + Assertions.assertThat(resourceRecords) + .isNotNull() + .isNotEmpty() + .containsAll(expected); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RestApiAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RestApiAssert.java new file mode 100644 index 0000000..5e8a394 --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RestApiAssert.java @@ -0,0 +1,48 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::ApiGateway::RestApi. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsRestApi(String)}. + */ +@SuppressWarnings("unchecked") +public class RestApiAssert extends AbstractCDKResourcesAssert> { + + private RestApiAssert(final Map actual) { + super(actual, RestApiAssert.class); + } + + public static RestApiAssert assertThat(final Map actual) { + return new RestApiAssert(actual); + } + + public RestApiAssert hasRestApi(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final String name = (String) properties.get("Name"); + + Assertions.assertThat(name) + .isNotBlank() + .matches(expected); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RoleAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RoleAssert.java new file mode 100644 index 0000000..472176d --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/RoleAssert.java @@ -0,0 +1,80 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::IAM::Role. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsRole(String)}. + */ +@SuppressWarnings("unchecked") +public class RoleAssert extends AbstractCDKResourcesAssert> { + + private RoleAssert(final Map actual) { + super(actual, RoleAssert.class); + } + + public static RoleAssert assertThat(final Map actual) { + return new RoleAssert(actual); + } + + public RoleAssert hasManagedPolicyArn(final String managedPolicyArnString) { + + final Map properties = (Map) actual.get("Properties"); + final List managedPolicyArns = (List) properties.get("ManagedPolicyArns"); + + // TODO flatten ManagedPolicyArns and test again string + + Assertions.assertThat(managedPolicyArns) + .isNotNull() + .hasSize(1) + .anySatisfy(s -> { + Assertions.assertThat(s) + .isInstanceOf(Map.class) + .extracting("Fn::Join") + .satisfies(joinList -> { + if (joinList instanceof List) { + Assertions.assertThat((List) joinList) + .flatExtracting(joinElement -> joinElement instanceof List ? (List) joinElement : Collections.emptyList()) + .anySatisfy(joinElement -> + Assertions.assertThat(joinElement) + .matches(e -> e.toString().matches(managedPolicyArnString))); + } + }); + }); + + return this; + } + + public RoleAssert hasAssumeRolePolicyDocument(final Map principal, + final String resource, + final String effect, + final String policyDocumentVersion, + final String action) { + + final Map properties = (Map) actual.get("Properties"); + final Map policyDocument = + (Map) properties.get("AssumeRolePolicyDocument"); + + return hasPolicy(principal, resource, effect, policyDocumentVersion, action, policyDocument); + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicAssert.java new file mode 100644 index 0000000..feacefe --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicAssert.java @@ -0,0 +1,99 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::SNS::Topic. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsTopic(String)}. + */ +@SuppressWarnings("unchecked") +public class TopicAssert extends AbstractCDKResourcesAssert> { + + private TopicAssert(final Map actual) { + super(actual, TopicAssert.class); + } + + public static TopicAssert assertThat(final Map actual) { + return new TopicAssert(actual); + } + + public TopicAssert hasTopicName(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + + final String topicName = (String) properties.get("TopicName"); + + Assertions.assertThat(topicName) + .containsPattern(expected); + + return this; + } + + public TopicAssert isFifo(final boolean expected) { + + final Map properties = (Map) actual.get("Properties"); + final Boolean isFifo = (Boolean) properties.get("FifoTopic"); + + Assertions.assertThat(isFifo) + .isEqualTo(expected); + + return this; + } + + public TopicAssert hasContentBasedDeduplicationEnabled(final boolean expected) { + + final Map properties = (Map) actual.get("Properties"); + final Boolean isFifo = properties.get("ContentBasedDeduplication"); + + Assertions.assertThat(isFifo) + .isEqualTo(expected); + + return this; + } + + public TopicAssert hasMessageRetentionPeriodInDays(final Number expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map archivePolicy = (Map) properties.get("ArchivePolicy"); + final Number messageRetentionPeriod = archivePolicy.get("MessageRetentionPeriod"); + + Assertions.assertThat(messageRetentionPeriod) + .isEqualTo(expected); + + return this; + } + + public TopicAssert hasMasterKey(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map kmsMasterKeyId = (Map) properties.get("KmsMasterKeyId"); + final List keyArnFun = (List) kmsMasterKeyId.get("Fn::GetAtt"); + + Assertions.assertThat(keyArnFun) + .isInstanceOf(List.class) + .anySatisfy(s -> Assertions.assertThat(s) + .isInstanceOf(String.class) + .containsPattern(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicSubscriptionAssert.java b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicSubscriptionAssert.java new file mode 100644 index 0000000..e2bea5f --- /dev/null +++ b/sandpipers-cdk-assertions/src/main/java/io/sandpipers/cdk/assertion/TopicSubscriptionAssert.java @@ -0,0 +1,87 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.assertion; + +import java.util.List; +import java.util.Map; +import org.assertj.core.api.Assertions; + +/** + * Fluent assertions for AWS::SNS::Subscription. This should be used if the resource map is extracted from the AWS template. Otherwise, start with + * {@link CDKStackAssert#containsTopicSubscription(String)}. + */ +@SuppressWarnings("unchecked") +public class TopicSubscriptionAssert extends + AbstractCDKResourcesAssert> { + + private TopicSubscriptionAssert(final Map actual) { + super(actual, TopicSubscriptionAssert.class); + } + + public static TopicSubscriptionAssert assertThat(final Map actual) { + return new TopicSubscriptionAssert(actual); + } + + public TopicSubscriptionAssert hasTopicArn(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final Map actualTopicArn = (Map) properties.get("TopicArn"); + + Assertions.assertThat(actualTopicArn.get("Ref").toString()) + .matches(expected); + + return this; + } + + public TopicSubscriptionAssert hasProtocol(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String actual = (String) properties.get("Protocol"); + + Assertions.assertThat(actual) + .matches(expected); + + return this; + } + + public TopicSubscriptionAssert hasEndpoint(final String expected) { + + final Map properties = (Map) actual.get("Properties"); + final String actual = (String) properties.get("Endpoint"); + + Assertions.assertThat(actual) + .matches(expected); + + return this; + } + + public TopicSubscriptionAssert hasDeadLetterQueue(final String expected) { + final Map properties = (Map) actual.get("Properties"); + final Map redrivePolicy = (Map) properties.get("RedrivePolicy"); + final Map deadLetterTargetArn = (Map) redrivePolicy.get("deadLetterTargetArn"); + final List deadLetterTargetArnFun = (List) deadLetterTargetArn.get("Fn::GetAtt"); + + Assertions.assertThat(deadLetterTargetArnFun) + .isInstanceOf(List.class) + .anySatisfy(s -> Assertions.assertThat(s) + .isInstanceOf(String.class) + .containsPattern(expected)); + + return this; + } +} diff --git a/sandpipers-cdk-assertions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sandpipers-cdk-assertions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 0000000..ca6ee9c --- /dev/null +++ b/sandpipers-cdk-assertions/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file diff --git a/sandpipers-cdk-bom/pom.xml b/sandpipers-cdk-bom/pom.xml new file mode 100644 index 0000000..eaa7b50 --- /dev/null +++ b/sandpipers-cdk-bom/pom.xml @@ -0,0 +1,79 @@ + + + 4.0.0 + + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + + + sandpipers-cdk-bom + pom + Sandpipers CDK BOM + Bill of materials for consistent dependencies across all sandpipers-cdk modules + https://github.com/sandpip3rs/sandpipers-cdk + + + 21 + 21 + UTF-8 + + + + + + io.sandpipers + sandpipers-cdk-assertions + ${project.version} + + + + io.sandpipers + sandpipers-cdk-constructs + ${project.version} + + + + io.sandpipers + sandpipers-cdk-types + ${project.version} + + + + + + + + matto + Muhammad Hamadto + https://github.com/muhamadto + + + + + + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + Licensed to Muhammad Hamadto + + 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 + https://www.apache.org/licenses/LICENSE-2.0 + + See the NOTICE file distributed with this work for additional information regarding + copyright ownership. + + 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. + + + + \ No newline at end of file diff --git a/sandpipers-cdk-constructs/pom.xml b/sandpipers-cdk-constructs/pom.xml new file mode 100644 index 0000000..de53f7c --- /dev/null +++ b/sandpipers-cdk-constructs/pom.xml @@ -0,0 +1,111 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + + + sandpipers-cdk-constructs + + + 21 + 21 + UTF-8 + + + + + + io.sandpipers + sandpipers-cdk-types + + + + + software.amazon.awscdk + aws-cdk-lib + + + + software.amazon.awscdk + apprunner-alpha + + + + + + org.apache.commons + commons-lang3 + + + + org.apache.commons + commons-collections4 + + + + com.google.guava + guava + + + + org.projectlombok + lombok + provided + + + + org.typefactory + type-factory-core + + + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + org.mockito + mockito-junit-jupiter + test + + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractApp.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractApp.java new file mode 100644 index 0000000..5817459 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractApp.java @@ -0,0 +1,45 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core; + +import io.sadpipers.cdk.type.SafeString; +import io.sandpipers.cdk.core.util.Constants; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.App; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.Tags; + +public abstract class AbstractApp extends App { + + public static void tagResources( + @NotNull final Stack stack, + @NotNull final AbstractEnvironment environment, + @NotNull final SafeString applicationName) { + Tags.of(stack) + .add(Constants.KEY_COST_CENTRE, Constants.VALUE_COST_CENTRE); + + Tags.of(stack) + .add(Constants.KEY_APPLICATION_NAME, applicationName.getValue()); + + Tags.of(stack) + .add(Constants.KEY_ENVIRONMENT, environment.getEnvironmentName().getValue()); + } + + @NotNull + public abstract SafeString getApplicationName(); +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractCostCentre.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractCostCentre.java new file mode 100644 index 0000000..3136929 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractCostCentre.java @@ -0,0 +1,57 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core; + +import static java.util.Objects.requireNonNull; + +import io.sadpipers.cdk.type.AlphanumericString; +import java.util.HashMap; +import java.util.Optional; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; + +/** + * Example usage CostCentre. + */ +@Getter +@SuperBuilder +public abstract class AbstractCostCentre { + + private static final HashMap costCentres = new HashMap<>(); + + @NotNull + private final AlphanumericString value; + + @NotNull + public String getValueAsString() { + return value.getValue(); + } + + public static AbstractCostCentre of(@NotNull final AlphanumericString costCentre) { + return Optional + .ofNullable(costCentres.get(costCentre)) + .orElseThrow(() -> new IllegalArgumentException("CostCentre not found for costCentre: '" + costCentre + "'")); + } + + public static void registerCostCentre(@NotNull final AbstractCostCentre costCentre) { + requireNonNull(costCentre, "'costCentre' must not be null"); + + costCentres.put(costCentre.getValue(), costCentre); + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractEnvironment.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractEnvironment.java new file mode 100644 index 0000000..9b4ea00 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/AbstractEnvironment.java @@ -0,0 +1,61 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core; + +import static java.util.Objects.requireNonNull; + +import io.sadpipers.cdk.type.SafeString; +import java.util.HashMap; +import java.util.Optional; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; + +/** + * Example usage Environment. + */ +@Getter +@SuperBuilder +public abstract class AbstractEnvironment { + + private static final HashMap environments = new HashMap<>(); + + @NotNull + private final software.amazon.awscdk.Environment awsEnvironment; + + @NotNull + private final AbstractCostCentre costCentre; + + @NotNull + private final SafeString environmentKey; + + @NotNull + private final SafeString environmentName; + + public static AbstractEnvironment of(@NotNull final SafeString environmentKey) { + return Optional + .ofNullable(environments.get(environmentKey)) + .orElseThrow(() -> new IllegalArgumentException("Environment not found for awsEnvironment: '" + environmentKey + "'")); + } + + public static void registerEnvironment(@NotNull final AbstractEnvironment abstractEnvironment) { + requireNonNull(abstractEnvironment, "'environmentKey' must not be null"); + + environments.put(abstractEnvironment.getEnvironmentKey(), abstractEnvironment); + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseConstruct.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseConstruct.java new file mode 100644 index 0000000..9d341dd --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseConstruct.java @@ -0,0 +1,32 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct; + +import software.amazon.awscdk.Stack; +import software.constructs.IConstruct; + +public interface BaseConstruct extends IConstruct { + + default String getAccount() { + return Stack.of(this).getAccount(); + } + + default String getRegion() { + return Stack.of(this).getRegion(); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseStack.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseStack.java new file mode 100644 index 0000000..cffe912 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/BaseStack.java @@ -0,0 +1,55 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct; + +import static io.sandpipers.cdk.core.util.Utils.kebabToCamel; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.AbstractEnvironment; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.DefaultStackSynthesizer; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.StackProps; + +public class BaseStack extends Stack { + + public BaseStack(@NotNull final AbstractApp app, @NotNull final AbstractEnvironment environment) { + this(app, createStackProps(app, environment)); + } + + public BaseStack(@NotNull final AbstractApp app, @NotNull final StackProps props) { + super(app, props.getStackName(), props); + } + + private static StackProps createStackProps(final AbstractApp app, final AbstractEnvironment environment) { + final String stackName = "%s%sStake" + .formatted(kebabToCamel(environment.getCostCentre().getValueAsString()), kebabToCamel(app.getApplicationName().getValue())); + + return StackProps.builder() + .synthesizer(createDefaultStackSynthesizer(environment)) + .stackName(stackName) + .env(environment.getAwsEnvironment()) + .build(); + } + + private static DefaultStackSynthesizer createDefaultStackSynthesizer(final AbstractEnvironment environment) { + return DefaultStackSynthesizer.Builder.create() + .qualifier(environment.getCostCentre().getValueAsString()) + .build(); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/LambdaRestApi.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/LambdaRestApi.java new file mode 100644 index 0000000..65da438 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/LambdaRestApi.java @@ -0,0 +1,70 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apigateway; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apigateway.LambdaRestApiProps; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::ApiGateway::RestApi (Lambda Rest Api) + *

Example usage can be found in sandpipers-cdk-example-lambda

+ */ +@Getter +public class LambdaRestApi extends Construct implements BaseConstruct { + + private software.amazon.awscdk.services.apigateway.LambdaRestApi lambdaRestApi; + + public LambdaRestApi( + @NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final LambdaRestApiProps props) { + super(scope, id.getValue()); + + this.lambdaRestApi = software.amazon.awscdk.services.apigateway.LambdaRestApi.Builder.create(this, id.getValue()) + .handler(props.getHandler()) + .integrationOptions(props.getIntegrationOptions()) + .proxy(props.getProxy()) + .deploy(props.getDeploy()) + .domainName(props.getDomainName()) + .policy(props.getPolicy()) + .defaultCorsPreflightOptions(props.getDefaultCorsPreflightOptions()) + .defaultIntegration(props.getDefaultIntegration()) + .defaultMethodOptions(props.getDefaultMethodOptions()) + .cloudWatchRole(props.getCloudWatchRole()) + .cloudWatchRoleRemovalPolicy(props.getCloudWatchRoleRemovalPolicy()) + .deployOptions(props.getDeployOptions()) + .description(props.getDescription()) + .disableExecuteApiEndpoint(props.getDisableExecuteApiEndpoint()) + .endpointExportName(props.getEndpointExportName()) + .endpointTypes(props.getEndpointTypes()) + .failOnWarnings(props.getFailOnWarnings()) + .parameters(props.getParameters()) + .retainDeployments(props.getRetainDeployments()) + .apiKeySourceType(props.getApiKeySourceType()) + .binaryMediaTypes(props.getBinaryMediaTypes()) + .cloneFrom(props.getCloneFrom()) + .endpointConfiguration(props.getEndpointConfiguration()) + .minCompressionSize(props.getMinCompressionSize()) + .build(); + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/RestApi.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/RestApi.java new file mode 100644 index 0000000..aed9bf7 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apigateway/RestApi.java @@ -0,0 +1,150 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apigateway; + +import io.sadpipers.cdk.type.SafeString; +import io.sandpipers.cdk.core.construct.BaseConstruct; +import java.util.List; +import java.util.Map; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.Size; +import software.amazon.awscdk.services.apigateway.ApiKeySourceType; +import software.amazon.awscdk.services.apigateway.CorsOptions; +import software.amazon.awscdk.services.apigateway.DomainNameOptions; +import software.amazon.awscdk.services.apigateway.EndpointConfiguration; +import software.amazon.awscdk.services.apigateway.EndpointType; +import software.amazon.awscdk.services.apigateway.IRestApi; +import software.amazon.awscdk.services.apigateway.Integration; +import software.amazon.awscdk.services.apigateway.MethodOptions; +import software.amazon.awscdk.services.apigateway.StageOptions; +import software.amazon.awscdk.services.iam.PolicyDocument; +import software.constructs.Construct; + +@Getter +public class RestApi extends Construct implements BaseConstruct { + + private software.amazon.awscdk.services.apigateway.RestApi restApi; + + public RestApi( + @NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final RestApiProps props) { + super(scope, id.getValue()); + + this.restApi = software.amazon.awscdk.services.apigateway.RestApi.Builder.create(this, id.getValue()) + .defaultCorsPreflightOptions(props.getDefaultCorsPreflightOptions()) + .defaultIntegration(props.getDefaultIntegration()) + .defaultMethodOptions(props.getDefaultMethodOptions()) + .cloudWatchRole(props.getCloudWatchRole()) + .cloudWatchRoleRemovalPolicy(props.getCloudWatchRoleRemovalPolicy()) + .deploy(props.getDeploy()) + .deployOptions(props.getDeployOptions()) + .description(props.getDescription()) + .disableExecuteApiEndpoint(props.getDisableExecuteApiEndpoint()) + .domainName(props.getDomainName()) + .endpointExportName(props.getEndpointExportName()) + .endpointTypes(props.getEndpointTypes()) + .failOnWarnings(props.getFailOnWarnings()) + .parameters(props.getParameters()) + .policy(props.getPolicy()) + .retainDeployments(props.getRetainDeployments()) + .apiKeySourceType(props.getApiKeySourceType()) + .binaryMediaTypes(props.getBinaryMediaTypes()) + .cloneFrom(props.getCloneFrom()) + .endpointConfiguration(props.getEndpointConfiguration()) + .minCompressionSize(props.getMinCompressionSize()) + .build(); + } + + @Getter + @SuperBuilder + public static class RestApiProps implements software.amazon.awscdk.services.apigateway.RestApiProps { + + @Nullable + private StageOptions deployOptions; + + @Nullable + private DomainNameOptions domainName; + + @Nullable + private CorsOptions defaultCorsPreflightOptions; + + @Nullable + private Integration defaultIntegration; + + @Nullable + private MethodOptions defaultMethodOptions; + + @Nullable + private Boolean cloudWatchRole; + + @Nullable + private RemovalPolicy cloudWatchRoleRemovalPolicy; + + @Nullable + private Boolean deploy; + + @Nullable + private String description; + + @Nullable + private Boolean disableExecuteApiEndpoint; + + @Nullable + private String endpointExportName; + + @Singular + private List endpointTypes; + + @Nullable + private Boolean failOnWarnings; + + @Nullable + private Map parameters; + + @Nullable + private PolicyDocument policy; + + @Nullable + private Boolean retainDeployments; + + @Nullable + private ApiKeySourceType apiKeySourceType; + + @Singular + private List binaryMediaTypes; + + @Nullable + private IRestApi cloneFrom; + + @Nullable + private EndpointConfiguration endpointConfiguration; + + @Nullable + @Range(from = 0, to = 10485760) + // non-negative between 0 and 10485760 (10M) bytes + private Size minCompressionSize; + } + +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractAppRunnerService.java new file mode 100644 index 0000000..1b84c94 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractAppRunnerService.java @@ -0,0 +1,206 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.apprunner.AbstractAppRunnerService.AppRunnerServiceProps; +import io.sadpipers.cdk.type.AWSArn; +import io.sadpipers.cdk.type.AWSEcrImageIdentifier; +import io.sadpipers.cdk.type.AppRunnerCpu; +import io.sadpipers.cdk.type.AppRunnerMemory; +import io.sadpipers.cdk.type.NumericString; +import io.sadpipers.cdk.type.Path; +import io.sadpipers.cdk.type.Protocol; +import io.sadpipers.cdk.type.SafeString; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.services.apprunner.CfnService; +import software.amazon.awscdk.services.apprunner.CfnService.EgressConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.HealthCheckConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.ImageConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.ImageRepositoryProperty; +import software.amazon.awscdk.services.apprunner.CfnService.IngressConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.InstanceConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.NetworkConfigurationProperty; +import software.amazon.awscdk.services.apprunner.CfnService.SourceConfigurationProperty; +import software.amazon.awscdk.services.apprunner.alpha.Secret; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.iam.ServicePrincipal; +import software.constructs.Construct; + +@Getter +public abstract class AbstractAppRunnerService extends Construct implements BaseConstruct { + + protected static final String INSTANCE_ROLE_PRINCIPAL = "tasks.apprunner.amazonaws.com"; + protected static final String INSTANCE_ROLE_ID_SUFFIX = "InstanceRole"; + protected static final String PARTITION_AWS = "aws"; + + protected CfnService service; + + public AbstractAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id) { + super(scope, id.getValue()); + } + + @NotNull + protected CfnService createService(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final T props) { + return createService(scope, id, props, null); + } + + @NotNull + protected CfnService createService( + @NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props, + @Nullable final AWSArn vpcConnectorArn) { + final NetworkConfigurationProperty networkConfiguration = createNetworkConfiguration(props.getEgressType(), vpcConnectorArn, + props.getPubliclyAccessible()); + + final SourceConfigurationProperty sourceConfigurationProperty = createSourceConfigurationProperty(props); + + final HealthCheckConfigurationProperty healthCheckConfigurationProperty = createHealthCheckConfigurationProperty(props); + + final InstanceConfigurationProperty instanceConfigurationProperty = createInstanceConfigurationProperty(id, props); + + return CfnService.Builder.create(scope, id.getValue()) + .networkConfiguration(networkConfiguration).sourceConfiguration(sourceConfigurationProperty) + .healthCheckConfiguration(healthCheckConfigurationProperty) + .instanceConfiguration(instanceConfigurationProperty) + .build(); + } + + @NotNull + protected SourceConfigurationProperty createSourceConfigurationProperty(@NotNull final T props) { + final ImageConfigurationProperty imageConfigurationProperty = ImageConfigurationProperty.builder() + .port(props.getPort().getValue()) + .runtimeEnvironmentSecrets(props.getPolicyStatements()) + .runtimeEnvironmentVariables(props.getEnvironmentVariables()) + .build(); + + final ImageRepositoryProperty imageRepositoryProperty = ImageRepositoryProperty.builder() + .imageIdentifier(props.getAwsEcrImageIdentifier().getValue()) + .imageRepositoryType(props.getImageRepositoryType().getValue()) + .imageConfiguration(imageConfigurationProperty).build(); + + return SourceConfigurationProperty.builder().imageRepository(imageRepositoryProperty).build(); + } + + @NotNull + protected InstanceConfigurationProperty createInstanceConfigurationProperty(@NotNull final SafeString id, @NotNull final T props) { + + final Role instanceRole = createRole(id, INSTANCE_ROLE_ID_SUFFIX, INSTANCE_ROLE_PRINCIPAL); + props.policyStatements.forEach(instanceRole::addToPolicy); + + return InstanceConfigurationProperty.builder() + .cpu(props.getCpu().getValue()) + .memory(props.getMemory().getValue()) + .instanceRoleArn(instanceRole.getRoleArn()).build(); + } + + @NotNull + protected HealthCheckConfigurationProperty createHealthCheckConfigurationProperty(@NotNull final T props) { + return HealthCheckConfigurationProperty.builder() + .protocol(props.getHealthCheckProtocol().getValue()) + .path(props.getHealthCheckPath().getValue()).interval(props.getHealthCheckInterval()) + .timeout(props.getHealthCheckTimeout()) + .unhealthyThreshold(props.getUnhealthyThreshold()) + .build(); + } + + private NetworkConfigurationProperty createNetworkConfiguration(final SafeString egressType, final AWSArn vpcConnectorArn, + final boolean isPubliclyAccessible) { + // TODO check if vpcConnectorArn is null and egressType is VPC + final EgressConfigurationProperty egressConfigurationProperty = EgressConfigurationProperty.builder() + .egressType(egressType.getValue()) + .vpcConnectorArn(vpcConnectorArn.getValue()) + .build(); + + final IngressConfigurationProperty ingressConfiguration = IngressConfigurationProperty.builder() + .isPubliclyAccessible(isPubliclyAccessible) + .build(); + + return NetworkConfigurationProperty.builder().egressConfiguration(egressConfigurationProperty) + .ingressConfiguration(ingressConfiguration) + .build(); + } + + private Role createRole(@NotNull final SafeString serviceId, final String idSuffix, final String assumedByPrincipal) { + final String id = serviceId + idSuffix; + + return Role.Builder.create(this, id) + .assumedBy(new ServicePrincipal(assumedByPrincipal)) + .build(); + } + + @Getter + @SuperBuilder + public static class AppRunnerServiceProps { + + @NotNull + private final AWSEcrImageIdentifier awsEcrImageIdentifier; + + @NotNull + private SafeString imageRepositoryType; + + @NotNull + private final Path healthCheckPath; + + @Default + private final NumericString port = NumericString.of("8080"); + + @Singular + private final Map environmentSecrets = new HashMap<>(); + + @Singular + private final List environmentVariables = new ArrayList<>(); + + @Singular + protected final List policyStatements = new ArrayList<>(); + + @Default + private final Number healthCheckInterval = 5; + + @Default + private final Number healthCheckTimeout = 2; + + @Default + private final Number unhealthyThreshold = 5; + + @Default + private final Protocol healthCheckProtocol = Protocol.of("HTTP"); + + @Default + protected final AppRunnerMemory memory = AppRunnerMemory.of("2 GB"); + + @Default + protected final AppRunnerCpu cpu = AppRunnerCpu.of("1 vCPU"); + + private final SafeString egressType; + + private final Boolean publiclyAccessible; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPrivateEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPrivateEgressAppRunnerService.java new file mode 100644 index 0000000..a4252d0 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPrivateEgressAppRunnerService.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.AbstractPrivateEgressAppRunnerService.PrivateEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.AWSArn; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apprunner.alpha.VpcConnector; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.ec2.SubnetType; +import software.constructs.Construct; + +@Getter +public abstract class AbstractPrivateEgressAppRunnerService extends AbstractAppRunnerService { + + public AbstractPrivateEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id) { + super(scope, id); + } + + @NotNull + protected AWSArn getVPCConnectorArn(@NotNull final Construct scope, @NotNull final T props) { + final VpcConnector vpcConnector = createVpcConnector(scope, props); + return AWSArn.of(vpcConnector.getVpcConnectorArn()); + } + + @NotNull + protected VpcConnector createVpcConnector(@NotNull final Construct scope, @NotNull final T props) { + final SubnetSelection subnetSelection = SubnetSelection.builder().subnetType(props.getSubnetType()).build(); + + final IVpc vpc = props.getVpc(); + return VpcConnector.Builder.create(scope, vpc.getVpcId() + "connector").vpc(vpc).vpcSubnets(subnetSelection).build(); + + } + + @Getter + @SuperBuilder + public static class PrivateEgressAppRunnerServiceProps extends AppRunnerServiceProps { + + @NotNull + private final IVpc vpc; + + @NotNull + private final SubnetType subnetType; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPublicEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPublicEgressAppRunnerService.java new file mode 100644 index 0000000..c6c0b02 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/AbstractPublicEgressAppRunnerService.java @@ -0,0 +1,39 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.AbstractPublicEgressAppRunnerService.PublicEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +@Getter +public abstract class AbstractPublicEgressAppRunnerService extends AbstractAppRunnerService { + + public AbstractPublicEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id) { + super(scope, id); + } + + @Getter + @SuperBuilder + public static abstract class PublicEgressAppRunnerServiceProps extends AppRunnerServiceProps { + + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPrivateEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPrivateEgressAppRunnerService.java new file mode 100644 index 0000000..c782b26 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPrivateEgressAppRunnerService.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.PrivateIngressPrivateEgressAppRunnerService.PrivateIngressPrivateEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +public class PrivateIngressPrivateEgressAppRunnerService extends + AbstractPrivateEgressAppRunnerService { + + public PrivateIngressPrivateEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final T props) { + super(scope, id); + + this.service = createService(scope, id, props, getVPCConnectorArn(scope, props)); + } + + @Getter + @SuperBuilder + public static class PrivateIngressPrivateEgressAppRunnerServiceProps extends PrivateEgressAppRunnerServiceProps { + + private final SafeString egressType = SafeString.of("VPC"); + + private final Boolean publiclyAccessible = false; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPublicEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPublicEgressAppRunnerService.java new file mode 100644 index 0000000..9220c34 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PrivateIngressPublicEgressAppRunnerService.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.PrivateIngressPublicEgressAppRunnerService.PrivateIngressPublicEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +public class PrivateIngressPublicEgressAppRunnerService extends + AbstractPublicEgressAppRunnerService { + + public PrivateIngressPublicEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final T props) { + super(scope, id); + + this.service = createService(scope, id, props); + } + + @Getter + @SuperBuilder + public static class PrivateIngressPublicEgressAppRunnerServiceProps extends PublicEgressAppRunnerServiceProps { + + private final SafeString egressType = SafeString.of("DEFAULT"); + + private final Boolean publiclyAccessible = false; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPrivateEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPrivateEgressAppRunnerService.java new file mode 100644 index 0000000..9ce03e8 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPrivateEgressAppRunnerService.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.PublicIngressPrivateEgressAppRunnerService.PublicIngressAbstractPrivateEgressAbstractAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +public class PublicIngressPrivateEgressAppRunnerService extends + AbstractPrivateEgressAppRunnerService { + + public PublicIngressPrivateEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final T props) { + super(scope, id); + + this.service = createService(scope, id, props, getVPCConnectorArn(scope, props)); + } + + @Getter + @SuperBuilder + public static class PublicIngressAbstractPrivateEgressAbstractAppRunnerServiceProps extends PrivateEgressAppRunnerServiceProps { + + private final SafeString egressType = SafeString.of("VPC"); + + private final Boolean publiclyAccessible = true; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPublicEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPublicEgressAppRunnerService.java new file mode 100644 index 0000000..9ae5ea9 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/PublicIngressPublicEgressAppRunnerService.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner; + +import io.sandpipers.cdk.core.construct.apprunner.PublicIngressPublicEgressAppRunnerService.PublicIngressPublicEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +public class PublicIngressPublicEgressAppRunnerService extends + AbstractPublicEgressAppRunnerService { + + public PublicIngressPublicEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final T props) { + super(scope, id); + + this.service = createService(scope, id, props); + } + + @Getter + @SuperBuilder + public static class PublicIngressPublicEgressAppRunnerServiceProps extends PublicEgressAppRunnerServiceProps { + + private final SafeString egressType = SafeString.of("DEFAULT"); + + private final Boolean publiclyAccessible = true; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractAppRunnerService.java new file mode 100644 index 0000000..c7914dd --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractAppRunnerService.java @@ -0,0 +1,151 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner.experimental; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.apprunner.experimental.AbstractAppRunnerService.AppRunnerServiceProps; +import io.sadpipers.cdk.type.Path; +import io.sadpipers.cdk.type.SafeString; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.Arn; +import software.amazon.awscdk.ArnComponents; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.services.apprunner.alpha.Cpu; +import software.amazon.awscdk.services.apprunner.alpha.EcrProps; +import software.amazon.awscdk.services.apprunner.alpha.EcrSource; +import software.amazon.awscdk.services.apprunner.alpha.HealthCheck; +import software.amazon.awscdk.services.apprunner.alpha.HttpHealthCheckOptions; +import software.amazon.awscdk.services.apprunner.alpha.ImageConfiguration; +import software.amazon.awscdk.services.apprunner.alpha.Memory; +import software.amazon.awscdk.services.apprunner.alpha.Secret; +import software.amazon.awscdk.services.apprunner.alpha.Service; +import software.amazon.awscdk.services.apprunner.alpha.Source; +import software.amazon.awscdk.services.ecr.IRepository; +import software.amazon.awscdk.services.ecr.Repository; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.iam.ServicePrincipal; +import software.constructs.Construct; + +@Getter +public abstract class AbstractAppRunnerService extends Construct implements BaseConstruct { + + protected static final String ACCESS_ROLE_PRINCIPAL = "build.apprunner.amazonaws.com"; + protected static final String ACCESS_ROLE_ID_SUFFIX = "ServiceRole"; + protected static final String INSTANCE_ROLE_PRINCIPAL = "tasks.apprunner.amazonaws.com"; + protected static final String INSTANCE_ROLE_ID_SUFFIX = "InstanceRole"; + protected static final String PARTITION_AWS = "aws"; + + protected Service service; + + public AbstractAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id) { + super(scope, id.getValue()); + } + + protected IRepository getRepository(final Construct scope, + final T props) { + + final String ecrRepositoryName = props.getEcrRepositoryName().getValue(); + + final ArnComponents arnComponents = ArnComponents.builder() + .partition(PARTITION_AWS) + .region(Stack.of(scope).getRegion()) + .account(Stack.of(scope).getAccount()) + .service("ecr") + .resource("repository") + .resourceName(ecrRepositoryName) + .build(); + + return Repository.fromRepositoryName(scope, ecrRepositoryName, Arn.format(arnComponents)); + } + + protected EcrSource createEcrSource(final T props, + final IRepository repository) { + final ImageConfiguration imageConfiguration = createImageConfiguration(props); + + return Source.fromEcr(EcrProps.builder() + .imageConfiguration(imageConfiguration) + .repository(repository) + .tagOrDigest("latest") + .build()); + } + + private ImageConfiguration createImageConfiguration(final T props) { + return ImageConfiguration.builder() + .port(props.getPort()) + .environmentSecrets(props.getEnvironmentSecrets()) + .build(); + } + + protected HealthCheck createHealthCheck(final T props) { + return HealthCheck.http(HttpHealthCheckOptions.builder() + .path(props.getHealthCheckPath().getValue()) + .interval(props.getHealthCheckInterval()) + .timeout(props.getHealthCheckTimeout()) + .build()); + } + + protected Role createRole(final String serviceId, + final String idSuffix, + final String assumedByPrincipal) { + return Role.Builder.create(this, serviceId + idSuffix) + .assumedBy(new ServicePrincipal(assumedByPrincipal)) + .build(); + } + + @Getter + @SuperBuilder + public static class AppRunnerServiceProps { + + @NotNull + private final SafeString ecrRepositoryName; + + @NotNull + private final Path healthCheckPath; + + @Default + private final Integer port = 8080; + + @Singular + private final Map environmentSecrets = new HashMap<>(); + + @Singular + protected final List policyStatements = new ArrayList<>(); + + @Default + private final Duration healthCheckInterval = Duration.seconds(5); + + @Default + private final Duration healthCheckTimeout = Duration.seconds(2); + + @Default + protected final Memory memory = Memory.TWO_GB; + + @Default + protected final Cpu cpu = Cpu.ONE_VCPU; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractPrivateEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractPrivateEgressAppRunnerService.java new file mode 100644 index 0000000..fab1ef9 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/AbstractPrivateEgressAppRunnerService.java @@ -0,0 +1,69 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner.experimental; + +import io.sandpipers.cdk.core.construct.apprunner.experimental.AbstractPrivateEgressAppRunnerService.PrivateEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apprunner.alpha.VpcConnector; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.ec2.SubnetType; +import software.constructs.Construct; + +@Getter +public abstract class AbstractPrivateEgressAppRunnerService extends + AbstractAppRunnerService { + + public AbstractPrivateEgressAppRunnerService(@NotNull final Construct scope, @NotNull final SafeString id) { + super(scope, id); + } + + protected VpcConnector createVpcConnector(final Construct scope, + final T props) { + final SubnetSelection subnetSelection = createSubnetSelection(props); + + final IVpc vpc = props.getVpc(); + + return VpcConnector.Builder + .create(scope, vpc.getVpcId() + "connector") + .vpc(vpc) + .vpcSubnets(subnetSelection) + .build(); + + } + + private SubnetSelection createSubnetSelection(final T props) { + return SubnetSelection.builder() + .subnetType(props.getSubnetType()) + .build(); + } + + @Getter + @SuperBuilder + public static class PrivateEgressAppRunnerServiceProps extends AppRunnerServiceProps { + + @NotNull + private final IVpc vpc; + + @NotNull + private final SubnetType subnetType; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPrivateEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPrivateEgressAppRunnerService.java new file mode 100644 index 0000000..f76a8c7 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPrivateEgressAppRunnerService.java @@ -0,0 +1,65 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner.experimental; + +import io.sandpipers.cdk.core.construct.apprunner.experimental.AbstractPrivateEgressAppRunnerService.PrivateEgressAppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apprunner.alpha.EcrSource; +import software.amazon.awscdk.services.apprunner.alpha.HealthCheck; +import software.amazon.awscdk.services.apprunner.alpha.Service; +import software.amazon.awscdk.services.apprunner.alpha.VpcConnector; +import software.amazon.awscdk.services.ecr.IRepository; +import software.amazon.awscdk.services.iam.Role; +import software.constructs.Construct; + +public class PublicIngressPrivateEgressAppRunnerService extends + AbstractPrivateEgressAppRunnerService { + + public PublicIngressPrivateEgressAppRunnerService(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id); + + final String serviceId = id.getValue(); + + final IRepository repository = getRepository(scope, props); + + final EcrSource ecrSource = createEcrSource(props, repository); + + final HealthCheck healthCheck = createHealthCheck(props); + + final VpcConnector vpcConnector = createVpcConnector(scope, props); + + final Role instanceRole = createRole(serviceId, INSTANCE_ROLE_ID_SUFFIX, INSTANCE_ROLE_PRINCIPAL); + props.policyStatements.forEach(instanceRole::addToPolicy); + + final Role accessRole = createRole(serviceId, ACCESS_ROLE_ID_SUFFIX, ACCESS_ROLE_PRINCIPAL); + repository.grantPull(accessRole); + + this.service = Service.Builder.create(scope, serviceId) + .source(ecrSource) + .healthCheck(healthCheck) + .vpcConnector(vpcConnector) + .instanceRole(instanceRole) + .accessRole(accessRole) + .memory(props.memory) + .cpu(props.cpu) + .build(); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPublicEgressAppRunnerService.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPublicEgressAppRunnerService.java new file mode 100644 index 0000000..9774593 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/apprunner/experimental/PublicIngressPublicEgressAppRunnerService.java @@ -0,0 +1,60 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.apprunner.experimental; + +import io.sandpipers.cdk.core.construct.apprunner.experimental.AbstractAppRunnerService.AppRunnerServiceProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apprunner.alpha.EcrSource; +import software.amazon.awscdk.services.apprunner.alpha.HealthCheck; +import software.amazon.awscdk.services.apprunner.alpha.Service; +import software.amazon.awscdk.services.ecr.IRepository; +import software.amazon.awscdk.services.iam.Role; +import software.constructs.Construct; + +public class PublicIngressPublicEgressAppRunnerService extends AbstractAppRunnerService { + + public PublicIngressPublicEgressAppRunnerService(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id); + + final String serviceId = id.getValue(); + + final IRepository repository = getRepository(scope, props); + + final EcrSource ecrSource = createEcrSource(props, repository); + + final HealthCheck healthCheck = createHealthCheck(props); + + final Role instanceRole = createRole(serviceId, INSTANCE_ROLE_ID_SUFFIX, INSTANCE_ROLE_PRINCIPAL); + props.policyStatements.forEach(instanceRole::addToPolicy); + + final Role accessRole = createRole(serviceId, ACCESS_ROLE_ID_SUFFIX, ACCESS_ROLE_PRINCIPAL); + repository.grantPull(accessRole); + + this.service = Service.Builder.create(scope, serviceId) + .source(ecrSource) + .healthCheck(healthCheck) + .instanceRole(instanceRole) + .accessRole(accessRole) + .memory(props.memory) + .cpu(props.cpu) + .build(); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/dynamodb/TableV2.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/dynamodb/TableV2.java new file mode 100644 index 0000000..e325be4 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/dynamodb/TableV2.java @@ -0,0 +1,112 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.dynamodb; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sadpipers.cdk.type.KebabCaseString; +import io.sadpipers.cdk.type.SafeString; +import java.util.List; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.Billing; +import software.amazon.awscdk.services.dynamodb.GlobalSecondaryIndexPropsV2; +import software.amazon.awscdk.services.dynamodb.LocalSecondaryIndexProps; +import software.amazon.awscdk.services.dynamodb.TableEncryptionV2; +import software.amazon.awscdk.services.dynamodb.TablePropsV2; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::DynamoDB::GlobalTable + *

Example usage can be found in sandpipers-cdk-example-dynamodb

+ */ +@Getter +public class TableV2 extends Construct implements BaseConstruct { + + private software.amazon.awscdk.services.dynamodb.TableV2 table; + + public TableV2( + @NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final TableProps props) { + super(scope, id.getValue()); + + this.table = software.amazon.awscdk.services.dynamodb.TableV2.Builder.create(this, id.getValue()) + .billing(props.getBilling()) + .tableName(props.getTableName()) + .encryption(props.getEncryption()) + .partitionKey(props.getPartitionKey()) + .removalPolicy(props.getRemovalPolicy()) + .sortKey(props.getSortKey()) + .deletionProtection(props.getDeletionProtection()) + .localSecondaryIndexes(props.getLocalSecondaryIndexes()) + .globalSecondaryIndexes(props.getGlobalSecondaryIndexes()) + .kinesisStream(props.getKinesisStream()) + .pointInTimeRecovery(props.getPointInTimeRecovery()) + .dynamoStream(props.getDynamoStream()) + .replicas(props.getReplicas()) + .contributorInsights(props.getContributorInsights()) + .build(); + } + + @Getter + @SuperBuilder + public static class TableProps implements TablePropsV2 { + + //TODO make it kebab case type + @NotNull + private final KebabCaseString tableName; + + @Default + private Billing billing = Billing.onDemand(); + + @Default + private RemovalPolicy removalPolicy = RemovalPolicy.RETAIN; + + @Default + private TableEncryptionV2 encryption = TableEncryptionV2.dynamoOwnedKey(); + + @NotNull + private Attribute partitionKey; + + @Singular + private List globalSecondaryIndexes; + + @Singular + private List localSecondaryIndexes; + + @Nullable + private Attribute sortKey; + + @Nullable + private String timeToLiveAttribute; + + @Nullable + private Boolean contributorInsights; + + public String getTableName() { + return tableName.getValue(); + } + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/ExistingVpc.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/ExistingVpc.java new file mode 100644 index 0000000..9b4dd55 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/ExistingVpc.java @@ -0,0 +1,58 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.ec2; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sadpipers.cdk.type.SafeString; +import java.util.HashMap; +import java.util.Map; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.Singular; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.VpcLookupOptions; +import software.constructs.Construct; + +@Getter +public class ExistingVpc extends Construct implements BaseConstruct { + + private final IVpc vpc; + + public ExistingVpc(@NotNull final Construct scope, @NotNull final SafeString id, @NotNull final VpcProps props) { + super(scope, id.getValue()); + + final VpcLookupOptions vpcLookupOptions = VpcLookupOptions.builder() + .ownerAccountId(this.getAccount()).region(this.getRegion()) + .isDefault(props.defaultVpc) + .tags(props.tags) + .build(); + + this.vpc = software.amazon.awscdk.services.ec2.Vpc.fromLookup(scope, id.getValue(), vpcLookupOptions); + } + + @Builder + public static class VpcProps implements software.amazon.awscdk.services.ec2.VpcProps { + + @Singular + private final Map tags = new HashMap<>(); + + @Default + private final Boolean defaultVpc = false; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/Vpc.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/Vpc.java new file mode 100644 index 0000000..424f663 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/ec2/Vpc.java @@ -0,0 +1,109 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.ec2; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sadpipers.cdk.type.IPv4Cidr; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder; +import lombok.Builder.Default; +import lombok.Getter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.IpAddresses; +import software.amazon.awscdk.services.ec2.IpProtocol; +import software.amazon.awscdk.services.ec2.Ipv6Addresses; +import software.amazon.awscdk.services.ec2.NatProvider; +import software.amazon.awscdk.services.ec2.SubnetConfiguration; +import software.amazon.awscdk.services.ec2.SubnetType; +import software.constructs.Construct; + +@Getter +public class Vpc extends Construct implements BaseConstruct { + + private final IVpc vpc; + + public Vpc(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final VpcProps props) { + super(scope, id.getValue()); + + final software.amazon.awscdk.services.ec2.Vpc.Builder vpcBuilder = software.amazon.awscdk.services.ec2.Vpc.Builder.create(this, id.getValue()) + .ipProtocol(props.getIpProtocol()) + .ipAddresses(IpAddresses.cidr(props.getIPv4Cidr().getValue())) + .maxAzs(props.getMaxAzs()) + .natGatewayProvider(props.getNatProvider()) + .natGateways(props.getNatGateways()); + + if (props.ipProtocol == IpProtocol.DUAL_STACK) { + vpcBuilder.ipv6Addresses(Ipv6Addresses.amazonProvided()); + } + + this.vpc = vpcBuilder.build(); + } + + private SubnetConfiguration createSubnetConfiguration(final String name, + final SubnetType subnetType, + final IpProtocol ipProtocol, + final boolean reserved, + final boolean mapPublicIpOnLaunch, + final int cidrMask) { + final SubnetConfiguration.Builder subnetConfigBuilder = SubnetConfiguration.builder() + // general properties + .name(name) + .subnetType(subnetType) + .reserved(reserved) + + // IPv4 specific properties + .mapPublicIpOnLaunch(mapPublicIpOnLaunch) + .cidrMask(cidrMask); + + // new IPv6 specific property + if (ipProtocol == IpProtocol.DUAL_STACK) { + subnetConfigBuilder.ipv6AssignAddressOnCreation(true); + } + + return subnetConfigBuilder.build(); + } + + @Getter + @Builder + public static class VpcProps implements software.amazon.awscdk.services.ec2.VpcProps { + + private static final software.amazon.awscdk.services.ec2.VpcProps VPC_PROPS = software.amazon.awscdk.services.ec2.VpcProps.builder().build(); + + @NotNull + private final IPv4Cidr iPv4Cidr; + + @NotNull + private final Number natGateways; + + @Default + @Nullable + private final NatProvider natProvider = NatProvider.gateway(); + + @Default + @Nullable + private final IpProtocol ipProtocol = VPC_PROPS.getIpProtocol(); + + @Default + @Nullable + private final Number maxAzs = VPC_PROPS.getMaxAzs(); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/AbstractCustomRuntimeFunction.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/AbstractCustomRuntimeFunction.java new file mode 100644 index 0000000..289b401 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/AbstractCustomRuntimeFunction.java @@ -0,0 +1,294 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.lambda; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.lambda.AbstractCustomRuntimeFunction.AbstractCustomRuntimeFunctionProps; +import io.sandpipers.cdk.core.construct.sns.Topic; +import io.sandpipers.cdk.core.construct.sns.Topic.TopicProps; +import io.sandpipers.cdk.core.construct.sqs.Queue; +import io.sandpipers.cdk.core.construct.sqs.Queue.QueueProps; +import io.sadpipers.cdk.type.SafeString; +import java.util.List; +import java.util.Map; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.Singular; +import lombok.experimental.SuperBuilder; +import org.apache.commons.lang3.BooleanUtils; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.Range; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.services.codeguruprofiler.IProfilingGroup; +import software.amazon.awscdk.services.ec2.ISecurityGroup; +import software.amazon.awscdk.services.ec2.IVpc; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.iam.IRole; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.kms.IKey; +import software.amazon.awscdk.services.lambda.AdotInstrumentationConfig; +import software.amazon.awscdk.services.lambda.Architecture; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.FileSystem; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.FunctionProps; +import software.amazon.awscdk.services.lambda.ICodeSigningConfig; +import software.amazon.awscdk.services.lambda.IEventSource; +import software.amazon.awscdk.services.lambda.ILayerVersion; +import software.amazon.awscdk.services.lambda.LambdaInsightsVersion; +import software.amazon.awscdk.services.lambda.LogRetentionRetryOptions; +import software.amazon.awscdk.services.lambda.LoggingFormat; +import software.amazon.awscdk.services.lambda.ParamsAndSecretsLayerVersion; +import software.amazon.awscdk.services.lambda.Runtime; +import software.amazon.awscdk.services.lambda.RuntimeManagementMode; +import software.amazon.awscdk.services.lambda.SnapStartConf; +import software.amazon.awscdk.services.lambda.Tracing; +import software.amazon.awscdk.services.lambda.VersionOptions; +import software.amazon.awscdk.services.logs.ILogGroup; +import software.amazon.awscdk.services.logs.RetentionDays; +import software.constructs.Construct; + +/** + * A lambda function with runtime provided.al2023 (custom runtime al2023). Creating function with any other runtime (e.g. passed in the + * {@link FunctionProps} or via any other means will be ignored). + */ + +@Getter +public class AbstractCustomRuntimeFunction extends Construct implements BaseConstruct { + + private static final int FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS = 10; + private static final int FUNCTION_DEFAULT_MEMORY_SIZE = 512; + private static final int FUNCTION_DEFAULT_RETRY_ATTEMPTS = 2; + private static final int FUNCTION_DEFAULT_MAX_EVENT_AGE = 60; + + private final Function function; + + /** + * @param scope This parameter is required. + * @param id This parameter is required. + * @param props This parameter is required. + */ + public AbstractCustomRuntimeFunction(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id.getValue()); + + final Function.Builder builder = Function.Builder.create(this, id.getValue()) + .runtime(props.getRuntime()) + .description(props.getDescription()) + .code(props.getCode()) + .handler(props.getHandler()) + .timeout(props.getTimeout()) + .memorySize(props.getMemorySize()) + .retryAttempts(props.getRetryAttempts()) + .maxEventAge(props.getMaxEventAge()) + .retryAttempts(props.getRetryAttempts()) + .role(props.getRole()) + .vpc(props.getVpc()) + .initialPolicy(props.getInitialPolicies()) + .environment(props.getEnvironment()); + + if (BooleanUtils.isTrue(props.getDeadLetterTopicEnabled())) { + final TopicProps topicProps = TopicProps.builder().build(); + + final Topic deadLetterTopic = new Topic<>(this, SafeString.of("DeadLetterTopic"), topicProps); + + builder.deadLetterTopic(deadLetterTopic.getTopic()); + } + + if (BooleanUtils.isTrue(props.getDeadLetterQueueEnabled())) { + final QueueProps queueProps = QueueProps.builder() + .requireDeadLetterQueue(false) + .deadLetterQueueMaxReceiveCount(12) + .build(); + + final Queue queue = new Queue<>(this, SafeString.of("DeadLetterQueue"), queueProps); + builder.deadLetterQueue(queue.getQueue()); + } + + function = builder + .build(); + + } + + @Getter + @SuperBuilder + public static class AbstractCustomRuntimeFunctionProps implements FunctionProps { + + private final Runtime runtime = null; + + @NotNull("may not be null") + private String description; + + @NotNull("may not be null") + private Code code; + + @NotNull("may not be null") + private String handler; + + @Default + private final Duration timeout = Duration.seconds(FUNCTION_DEFAULT_TIMEOUT_IN_SECONDS); + + @Default + @Range(from = 128, to = 3008) + private final Number memorySize = FUNCTION_DEFAULT_MEMORY_SIZE; + + @Default + @Range(from = 0, to = 2) + private final Integer retryAttempts = FUNCTION_DEFAULT_RETRY_ATTEMPTS; + + @Default + @Range(from = 60, to = 21600) + private Number maxEventAge = FUNCTION_DEFAULT_MAX_EVENT_AGE; + + @Nullable + @Default + private IRole role = null; + + @Nullable + @Default + private IVpc vpc = null; + + @Nullable + @Default + private Map environment = null; + + @Default + @NotNull + private Boolean deadLetterTopicEnabled = false; + + @Default + @NotNull + private Boolean deadLetterQueueEnabled = false; + + @Default + @NotNull + private Boolean failureDestinationRequired = false; + + @Default + @NotNull + private Boolean successDestinationRequired = false; + + @Nullable + @Singular + private List initialPolicies; + + @Nullable + private AdotInstrumentationConfig adotInstrumentation; + + @Nullable + private Boolean allowAllOutbound; + + @Nullable + private Boolean allowPublicSubnet; + + @Nullable + private String applicationLogLevel; + + @Nullable + private Architecture architecture; + + @Nullable + private ICodeSigningConfig codeSigningConfig; + + @Nullable + private VersionOptions currentVersionOptions; + + @Nullable + private IKey environmentEncryption; + + @Nullable + private software.amazon.awscdk.Size ephemeralStorageSize; + + @Nullable + private List events; + + @Nullable + private FileSystem filesystem; + + @Nullable + private LambdaInsightsVersion insightsVersion; + + @Nullable + private java.util.List layers; + + @Nullable + private String logFormat; + + @Nullable + private LoggingFormat loggingFormat; + + @Nullable + private ILogGroup logGroup; + + @Nullable + private RetentionDays logRetention; + + @Nullable + private LogRetentionRetryOptions logRetentionRetryOptions; + + @Nullable + private IRole logRetentionRole; + + @Nullable + private ParamsAndSecretsLayerVersion paramsAndSecrets; + + @Nullable + private Boolean profiling; + + @Nullable + private IProfilingGroup profilingGroup; + + @Nullable + private java.lang.Number reservedConcurrentExecutions; + + @Nullable + private RuntimeManagementMode runtimeManagementMode; + + @Nullable + private List securityGroups; + + @Nullable + private SnapStartConf snapStart; + + @Nullable + private String systemLogLevel; + + @Nullable + private Tracing tracing; + + @Nullable + private SubnetSelection vpcSubnets; + + @NotNull + public String getHandler() { + return handler; + } + + @NotNull + public String getDescription() { + return description; + } + + @NotNull + public Duration getMaxEventAge() { + return Duration.seconds(maxEventAge.intValue()); + } + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023Function.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023Function.java new file mode 100644 index 0000000..9ff197a --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023Function.java @@ -0,0 +1,58 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.lambda; + +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; + +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.lambda.Runtime; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::Lambda::Function (custom runtime al2023) + *

Example usage can be found in sandpipers-cdk-example-lambda

+ */ + +@Getter +public class CustomRuntime2023Function extends AbstractCustomRuntimeFunction { + + + /** + * @param scope This parameter is required. + * @param id This parameter is required. + * @param props This parameter is required. + */ + public CustomRuntime2023Function(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class CustomRuntime2023FunctionProps extends AbstractCustomRuntimeFunctionProps { + + private final Runtime runtime = PROVIDED_AL2023; + + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/AbstractARecord.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/AbstractARecord.java new file mode 100644 index 0000000..842e953 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/AbstractARecord.java @@ -0,0 +1,98 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.route53; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.route53.AbstractARecord.ARecordProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.services.route53.ARecord.Builder; +import software.amazon.awscdk.services.route53.HostedZone; +import software.amazon.awscdk.services.route53.HostedZoneProviderProps; +import software.amazon.awscdk.services.route53.IHostedZone; +import software.amazon.awscdk.services.route53.RecordSet; +import software.amazon.awscdk.services.route53.RecordTarget; +import software.constructs.Construct; + +@Getter +public abstract class AbstractARecord extends Construct implements BaseConstruct { + + private final RecordSet recordSet; + + public AbstractARecord(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id.getValue()); + + final Builder builder = Builder.create(this, id.getValue()) + .ttl(props.getTtl()) + .recordName(props.getRecordName()) + .target(props.getTarget()); + + this.recordSet = props.getZone() == null + ? builder.zone(getHostedZone(props)).build() + : builder.zone(props.getZone()).build(); + + this.recordSet.applyRemovalPolicy(props.getRemovalPolicy()); + } + + @NotNull + private IHostedZone getHostedZone(final ARecordProps props) { + + return HostedZone.fromLookup(this, "HostedZone", HostedZoneProviderProps.builder() + .domainName(props.getDomainName()) + .privateZone(props.getPrivateZone()) + .vpcId(props.getVpcId()) + .build()); + } + + @Getter + @SuperBuilder + public static class ARecordProps implements software.amazon.awscdk.services.route53.ARecordProps { + + private final Boolean privateZone = false; + + private final String domainName = null; + + @NotNull + private final String recordName; + + @NotNull + private final String vpcId; + + @NotNull + private RecordTarget target; + + @Default + private final RemovalPolicy removalPolicy = RemovalPolicy.DESTROY; + + @Default + @Nullable + private final IHostedZone zone = null; + + @Default + @Nullable + private final Duration ttl = Duration.minutes(30); + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PrivateARecord.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PrivateARecord.java new file mode 100644 index 0000000..a2b18ab --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PrivateARecord.java @@ -0,0 +1,48 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.route53; + +import io.sandpipers.cdk.core.construct.route53.AbstractARecord.ARecordProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::Route53::RecordSet (A Record) for private zone + *

Example usage can be found in sandpipers-cdk-example-route53

+ */ +public class PrivateARecord extends AbstractARecord { + + public PrivateARecord(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + @Getter + @SuperBuilder + public static class PrivateARecordProps extends ARecordProps { + + private final Boolean privateZone = true; + + @Default + private final String domainName = "internal.sandpipers.com"; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PublicARecord.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PublicARecord.java new file mode 100644 index 0000000..812ccba --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/route53/PublicARecord.java @@ -0,0 +1,50 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.route53; + +import io.sandpipers.cdk.core.construct.route53.PublicARecord.PublicARecordProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::Route53::RecordSet (A Record) for public zone + *

Example usage can be found in sandpipers-cdk-example-route53

+ */ +public class PublicARecord extends AbstractARecord { + + public PublicARecord(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class PublicARecordProps extends ARecordProps { + + private final Boolean privateZone = false; + + @Default + @NotNull + private final String domainName = "services.sandpipers.com"; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/AbstractTopic.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/AbstractTopic.java new file mode 100644 index 0000000..6f618aa --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/AbstractTopic.java @@ -0,0 +1,76 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sns; + +import static software.amazon.awscdk.services.sns.Topic.Builder.create; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.sns.AbstractTopic.AbstractTopicProps; +import io.sadpipers.cdk.type.SafeString; +import java.util.List; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.services.kms.IKey; +import software.amazon.awscdk.services.sns.ITopic; +import software.amazon.awscdk.services.sns.LoggingConfig; +import software.constructs.Construct; + +@Getter +public class AbstractTopic extends Construct implements BaseConstruct { + + private final ITopic topic; + + public AbstractTopic(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id.getValue()); + + topic = create(this, id.getValue()) + .fifo(props.getFifo()) + .contentBasedDeduplication(props.getContentBasedDeduplication()) + .masterKey(props.getMasterKey()) + .loggingConfigs(props.getLoggingConfigs()) + .messageRetentionPeriodInDays(props.getMessageRetentionPeriodInDays()) + .build(); + } + + @Getter + @SuperBuilder + public static class AbstractTopicProps implements software.amazon.awscdk.services.sns.TopicProps { + + protected static final software.amazon.awscdk.services.sns.TopicProps TOPIC_PROPS = + software.amazon.awscdk.services.sns.TopicProps.builder().build(); + + private final Boolean fifo = false; + + @Nullable + @Default + private final IKey masterKey = TOPIC_PROPS.getMasterKey(); + + @Nullable + @Default + private final List loggingConfigs = TOPIC_PROPS.getLoggingConfigs(); + + @Nullable + @Default + private final Number messageRetentionPeriodInDays = TOPIC_PROPS.getMessageRetentionPeriodInDays(); + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/FifoTopic.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/FifoTopic.java new file mode 100644 index 0000000..6ca1dde --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/FifoTopic.java @@ -0,0 +1,50 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sns; + +import io.sandpipers.cdk.core.construct.sns.FifoTopic.FifoTopicProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::SNS::Topic (FIFO) + *

Example usage can be found in sandpipers-cdk-example-sns

+ */ +@Getter +public class FifoTopic extends AbstractTopic { + + public FifoTopic(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class FifoTopicProps extends AbstractTopicProps { + + private final Boolean fifo = true; + + @Default + private final Boolean contentBasedDeduplication = true; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/Topic.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/Topic.java new file mode 100644 index 0000000..c70ec81 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sns/Topic.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sns; + +import io.sandpipers.cdk.core.construct.sns.Topic.TopicProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::SNS::Topic (not FIFO) + *

Example usage can be found in sandpipers-cdk-example-sns

+ */ +public class Topic extends AbstractTopic { + + public Topic(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class TopicProps extends AbstractTopicProps { + + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/AbstractQueue.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/AbstractQueue.java new file mode 100644 index 0000000..2ffed0f --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/AbstractQueue.java @@ -0,0 +1,164 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sqs; + +import static io.sandpipers.cdk.core.util.Utils.kebabToCamel; +import static software.amazon.awscdk.services.sqs.Queue.Builder.create; +import static software.amazon.awscdk.services.sqs.RedrivePermission.DENY_ALL; + +import io.sandpipers.cdk.core.construct.BaseConstruct; +import io.sandpipers.cdk.core.construct.sqs.AbstractQueue.AbstractQueueProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.RemovalPolicy; +import software.amazon.awscdk.services.kms.IKey; +import software.amazon.awscdk.services.sqs.DeadLetterQueue; +import software.amazon.awscdk.services.sqs.IQueue; +import software.amazon.awscdk.services.sqs.Queue; +import software.amazon.awscdk.services.sqs.QueueEncryption; +import software.amazon.awscdk.services.sqs.RedriveAllowPolicy; +import software.constructs.Construct; + +@Getter +public class AbstractQueue extends Construct implements BaseConstruct { + + protected static final int DEAD_LETTER_QUEUE_MAX_RECEIVE_COUNT = 3; + + private final IQueue queue; + private final DeadLetterQueue deadLetterQueue; + + public AbstractQueue(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id.getValue()); + + final String idValue = kebabToCamel(id.getValue()); + + deadLetterQueue = props.getRequireDeadLetterQueue() ? createDeadLetterQueue("DeadLetterQueue", props) : null; + queue = createQueue(idValue, props, deadLetterQueue); + } + + @NotNull + private DeadLetterQueue createDeadLetterQueue(final String id, final T props) { + final Queue queue = getQueueBuilder(id, props) + .build(); + + return DeadLetterQueue.builder() + .maxReceiveCount(props.getDeadLetterQueueMaxReceiveCount()) + .queue(queue) + .build(); + } + + public Queue createQueue(final String id, final T props, final DeadLetterQueue deadLetterQueue) { + + return getQueueBuilder(id, props) + .deadLetterQueue(deadLetterQueue) + .redriveAllowPolicy(RedriveAllowPolicy.builder().redrivePermission(DENY_ALL).build()) + .build(); + } + + private Queue.Builder getQueueBuilder(final String id, final T props) { + return create(this, id) + .fifo(props.getFifo()) + .deduplicationScope(props.getDeduplicationScope()) + .contentBasedDeduplication(props.getContentBasedDeduplication()) + .fifoThroughputLimit(props.getFifoThroughputLimit()) + .maxMessageSizeBytes(props.getMaxMessageSizeBytes()) + .receiveMessageWaitTime(props.getReceiveMessageWaitTime()) + .visibilityTimeout(props.getDeliveryDelay()) + .removalPolicy(props.getRemovalPolicy()) + .enforceSsl(props.getEnforceSSL()) + .encryptionMasterKey(props.getEncryptionMasterKey()) + .encryption(props.getEncryption()) + .dataKeyReuse(props.getDataKeyReuse()) + .deliveryDelay(props.getDeliveryDelay()) + .retentionPeriod(props.getRetentionPeriod()); + } + + @Getter + @SuperBuilder + public static class AbstractQueueProps implements software.amazon.awscdk.services.sqs.QueueProps { + + + private final Boolean fifo = false; + + /** + * This value is intentionally {@code null}. The dead-letter queue will be automatically created and assigned to the queue. Refer to + * {@link AbstractQueue#createDeadLetterQueue(String, AbstractQueueProps)} + */ + private final DeadLetterQueue deadLetterQueue = null; + + @Default + @NotNull + private final Boolean requireDeadLetterQueue = true; + + @Default + @NotNull + private final RemovalPolicy removalPolicy = RemovalPolicy.RETAIN; + + @Default + @NotNull + private final Boolean enforceSSL = true; + + @Default + @NotNull + private final QueueEncryption encryption = QueueEncryption.SQS_MANAGED; + + @Default + @Nullable + private final Number deadLetterQueueMaxReceiveCount = DEAD_LETTER_QUEUE_MAX_RECEIVE_COUNT; + + @Default + @Nullable + private final Duration deliveryDelay = Duration.seconds(0); + + @Default + @Nullable + private final Duration retentionPeriod = Duration.days(14); + + @Default + @Nullable + private final Number maxMessageSizeBytes = null; + + @Default + @Nullable + private final Duration receiveMessageWaitTime = null; + + @Default + @Nullable + private final Duration visibilityTimeout = null; + + @Default + @Nullable + private final IKey encryptionMasterKey = null; + + @Default + @Nullable + private final Duration dataKeyReuse = null; + + @Default + @Nullable + private final RedriveAllowPolicy redriveAllowPolicy = null; + + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/FifoQueue.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/FifoQueue.java new file mode 100644 index 0000000..8794cd3 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/FifoQueue.java @@ -0,0 +1,57 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sqs; + +import io.sandpipers.cdk.core.construct.sqs.FifoQueue.FifoQueueProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Builder.Default; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.sqs.DeduplicationScope; +import software.amazon.awscdk.services.sqs.FifoThroughputLimit; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::SQS::Queue (FIFO) + *

Example usage can be found in sandpipers-cdk-example-sqs

+ */ +public class FifoQueue extends AbstractQueue { + + public FifoQueue(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class FifoQueueProps extends AbstractQueueProps { + + private final Boolean fifo = true; + + @Default + private final Boolean contentBasedDeduplication = true; + + @Default + private final DeduplicationScope deduplicationScope = DeduplicationScope.MESSAGE_GROUP; + + @Default + private final FifoThroughputLimit fifoThroughputLimit = FifoThroughputLimit.PER_MESSAGE_GROUP_ID; + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/Queue.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/Queue.java new file mode 100644 index 0000000..46dfdb3 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/construct/sqs/Queue.java @@ -0,0 +1,44 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.sqs; + +import io.sandpipers.cdk.core.construct.sqs.Queue.QueueProps; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; +import org.jetbrains.annotations.NotNull; +import software.constructs.Construct; + +/** + * L3 Construct representing AWS::SQS::Queue (not FIFO) + *

Example usage can be found in sandpipers-cdk-example-sqs

+ */ +public class Queue extends AbstractQueue { + + public Queue(@NotNull final Construct scope, + @NotNull final SafeString id, + @NotNull final T props) { + super(scope, id, props); + } + + @Getter + @SuperBuilder + public static class QueueProps extends AbstractQueueProps { + + } +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Constants.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Constants.java new file mode 100644 index 0000000..1e52d52 --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Constants.java @@ -0,0 +1,30 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.util; + +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class Constants { + + public static final String KEY_COST_CENTRE = "COST_CENTRE"; + public static final String KEY_APPLICATION_NAME = "APPLICATION_NAME"; + public static final String KEY_ENVIRONMENT = "ENVIRONMENT"; + public static final String VALUE_COST_CENTRE = "Sandpipers"; + public static String AWS_REGION_AP_SOUTHEAST_2 = "ap-southeast-2"; +} diff --git a/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Utils.java b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Utils.java new file mode 100644 index 0000000..2afc2ee --- /dev/null +++ b/sandpipers-cdk-constructs/src/main/java/io/sandpipers/cdk/core/util/Utils.java @@ -0,0 +1,37 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.util; + +import lombok.NoArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +@NoArgsConstructor(access = lombok.AccessLevel.PRIVATE) +public class Utils { + + public static String prefixId(final String application, final String id) { + return "%s%s%s".formatted(Constants.VALUE_COST_CENTRE, application, id); + } + + public static String kebabToCamel(final String kebabCaseString) { + String[] words = StringUtils.split(kebabCaseString, '-'); + for (int i = 1; i < words.length; i++) { + words[i] = StringUtils.capitalize(words[i]); + } + return StringUtils.capitalize(StringUtils.join(words)); + } +} diff --git a/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023FunctionTest.java b/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023FunctionTest.java new file mode 100644 index 0000000..3b760db --- /dev/null +++ b/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/CustomRuntime2023FunctionTest.java @@ -0,0 +1,222 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.lambda; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static software.amazon.awscdk.services.lambda.Architecture.ARM_64; +import static software.amazon.awscdk.services.lambda.CodeSigningConfig.fromCodeSigningConfigArn; +import static software.amazon.awscdk.services.lambda.Runtime.PROVIDED_AL2023; + +import io.sandpipers.cdk.core.construct.lambda.AbstractCustomRuntimeFunction.AbstractCustomRuntimeFunctionProps; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps.CustomRuntime2023FunctionPropsBuilder; +import io.sadpipers.cdk.type.SafeString; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import software.amazon.awscdk.App; +import software.amazon.awscdk.Duration; +import software.amazon.awscdk.Size; +import software.amazon.awscdk.Stack; +import software.amazon.awscdk.services.codeguruprofiler.ProfilingGroup; +import software.amazon.awscdk.services.ec2.SecurityGroup; +import software.amazon.awscdk.services.ec2.SubnetSelection; +import software.amazon.awscdk.services.ec2.Vpc; +import software.amazon.awscdk.services.iam.PolicyStatement; +import software.amazon.awscdk.services.iam.Role; +import software.amazon.awscdk.services.kms.Key; +import software.amazon.awscdk.services.lambda.Code; +import software.amazon.awscdk.services.lambda.Function; +import software.amazon.awscdk.services.lambda.Tracing; +import software.amazon.awscdk.services.lambda.VersionOptions; +import software.amazon.awscdk.services.lambda.eventsources.ApiEventSource; +import software.amazon.awscdk.services.logs.RetentionDays; + +@Disabled +class CustomRuntime2023FunctionTest { + + @TempDir + private static Path TEMP_DIR; + private Stack stack; + private SafeString id; + private Path lambdaCodePath; + private CustomRuntime2023FunctionPropsBuilder functionPropsBuilder; + + @BeforeEach + void setUp() throws IOException { + lambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(TEMP_DIR); + + stack = new Stack(new App(), "test-stack"); + id = SafeString.of("test-function"); + + functionPropsBuilder = CustomRuntime2023FunctionProps.builder() + .maxEventAge(514) + .failureDestinationRequired(true) + .successDestinationRequired(true) + .retryAttempts(2) + .allowPublicSubnet(false) + .architecture(ARM_64) + .codeSigningConfig(fromCodeSigningConfigArn(stack, "test-code-signing-config", "arn:aws:lambda:us-east-1:***:code-signing-config:***")) + .currentVersionOptions(VersionOptions.builder().build()) + .deadLetterQueueEnabled(false) + .deadLetterTopicEnabled(false) + .description("test function") + .environment(Map.of("Account", "***")) + .environmentEncryption(Key.fromKeyArn(stack, "test-key", "arn:aws:kms:us-east-1:***:key/***")) + .ephemeralStorageSize(Size.gibibytes(1)) + .events(List.of(new ApiEventSource("POST", "/test"))) + .filesystem(null) + .initialPolicy(PolicyStatement.Builder.create().build()) + .insightsVersion(null) + .layers(Collections.emptyList()) + .logRetention(RetentionDays.FIVE_DAYS) + .logRetentionRole(Role.fromRoleArn(stack, "test-log-role", "arn:aws:iam::***:role/test-log-role")) + .memorySize(512) + .profiling(false) + .profilingGroup(ProfilingGroup.fromProfilingGroupName(stack, "test-profiling-group", "test-profiling-group")) + .reservedConcurrentExecutions(2) + .role(Role.fromRoleArn(stack, "test-role", "arn:aws:iam::***:role/test-role")) + .securityGroups(List.of(SecurityGroup.fromSecurityGroupId(stack, "test-security-group", "sg-***"))) + .tracing(Tracing.ACTIVE) + .vpc(Vpc.Builder.create(stack, "test-vpc").build()) + .vpcSubnets(SubnetSelection.builder().build()) + .code(Code.fromAsset(lambdaCodePath.toString())) + .handler("io.sandpipers.springnativeawslambda.infra.lambda.CustomRuntime2Function::handleRequest"); + } + + @Test + void should_create_and_return_function() { + final CustomRuntime2023FunctionProps functionProps = functionPropsBuilder.build(); + final Function actual = new CustomRuntime2023Function<>(stack, id, functionProps).getFunction(); + + assertThat(actual) + .isNotNull(); + + assertThat(actual.getRuntime()) + .isEqualTo(PROVIDED_AL2023); + } + + @Test + void should_create_and_return_function_with_non_default_timeout() { + final Duration timeout = Duration.seconds(3); + final CustomRuntime2023FunctionProps functionProps = (CustomRuntime2023FunctionProps) functionPropsBuilder + .timeout(timeout) + .build(); + + final Function actual = new CustomRuntime2023Function<>(stack, id, functionProps).getFunction(); + + assertThat(actual) + .isNotNull(); + + assertThat(actual.getTimeout() + .toSeconds()) + .isEqualTo(3); + } + + @Test + @Disabled + void should_throw_exception_when_function_handler_is_null() { + + assertThatThrownBy(() -> functionPropsBuilder.handler(null).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("may not be null"); + } + + @Test + void should_throw_exception_when_function_code_is_null() { + + assertThatThrownBy(() -> functionPropsBuilder.code(null).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("may not be null"); + } + + @Test + void should_throw_exception_when_function_description_is_null() { + assertThatThrownBy(() -> functionPropsBuilder.description(null).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("may not be null"); + } + + @Test + @Disabled + void should_throw_exception_when_function_memory_size_is_less_than_128() { + + assertThatThrownBy(() -> functionPropsBuilder.memorySize(120).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'memorySize' must be between 128 and 3008 (inclusive)"); + } + + @Test + @Disabled + void should_throw_exception_when_function_memory_size_is_larger_than_3008() { + assertThatThrownBy(() -> functionPropsBuilder.memorySize(3500).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'memorySize' must be between 128 and 3008 (inclusive)"); + } + + @Test + @Disabled + void should_throw_exception_when_function_memory_size_is_less_than_zero() { + assertThatThrownBy(() -> functionPropsBuilder.retryAttempts(-1).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'retryAttempts' must be between 128 and 3008 (inclusive)"); + } + + @Test + @Disabled + void should_throw_exception_when_function_memory_size_is_larger_than_two() { + assertThatThrownBy(() -> functionPropsBuilder.retryAttempts(3).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'retryAttempts' must be between 128 and 3008 (inclusive)"); + } + + @Test + @Disabled + void should_throw_exception_when_max_event_age_is_less_than_60() { + assertThatThrownBy(() -> functionPropsBuilder.maxEventAge(59).build()) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("'retryAttempts' must be between 60 and 21600 (inclusive)"); + } + + @Test + @Disabled + void should_throw_exception_when_max_event_age_is_less_than_21600() { + + final AbstractCustomRuntimeFunctionProps props = functionPropsBuilder.maxEventAge(21601).build(); + + assertThatThrownBy(() -> new CustomRuntime2023Function<>(stack, id, (CustomRuntime2023FunctionProps)props)) + .isNotNull() + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("`maximumEventAge` must represent a `Duration` that is between 60 and 21600 seconds"); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/TestLambdaUtils.java b/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/TestLambdaUtils.java new file mode 100644 index 0000000..c36a9b9 --- /dev/null +++ b/sandpipers-cdk-constructs/src/test/java/io/sandpipers/cdk/core/construct/lambda/TestLambdaUtils.java @@ -0,0 +1,45 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.core.construct.lambda; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TestLambdaUtils { + + public static Path getTestLambdaCodePath(final Path tempDir) throws IOException { + final Path lambdaCodePath = tempDir.resolve("lambda-package.zip"); + + final File file = lambdaCodePath.toFile(); + + if (file.exists()) { + return lambdaCodePath; + } + + final boolean isCreated = file.createNewFile(); + + if (!isCreated) { + throw new IOException("Failed to create lambda package"); + } + return lambdaCodePath; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/pom.xml b/sandpipers-cdk-examples/pom.xml new file mode 100644 index 0000000..01e9f88 --- /dev/null +++ b/sandpipers-cdk-examples/pom.xml @@ -0,0 +1,77 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + + + sandpipers-cdk-examples + pom + + + 21 + 21 + UTF-8 + + + + sandpipers-cdk-example-sqs + sandpipers-cdk-example-sns + sandpipers-cdk-example-lambda + sandpipers-cdk-example-route53 + sandpipers-cdk-example-dynamodb + sandpipers-cdk-example-apigateway + + + + + + io.sandpipers + sandpipers-cdk-constructs + 1.0-SNAPSHOT + + + + io.sandpipers + sandpipers-cdk-assertions + 1.0-SNAPSHOT + + + + + + ${project.artifactId} + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + true + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/pom.xml new file mode 100644 index 0000000..efe8c52 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-apigateway + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Application.java new file mode 100644 index 0000000..3984764 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Application.java @@ -0,0 +1,49 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import static io.sandpipers.cdk.example.apigateway.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("apigateway-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final LambdaRestApiStack lambdaStack = new LambdaRestApiStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(lambdaStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + final RestApiStack restApiStack = new RestApiStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(restApiStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/CostCentre.java new file mode 100644 index 0000000..78543fc --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Environment.java new file mode 100644 index 0000000..0d3db1d --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/Environment.java @@ -0,0 +1,53 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; +import static io.sandpipers.cdk.example.apigateway.CostCentre.SANDPIPERS; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiStack.java new file mode 100644 index 0000000..79976c6 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiStack.java @@ -0,0 +1,62 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; +import io.sadpipers.cdk.type.SafeString; +import java.io.IOException; +import java.nio.file.Path; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apigateway.LambdaRestApiProps; +import software.amazon.awscdk.services.apigateway.StageOptions; +import software.amazon.awscdk.services.lambda.Code; + +public class LambdaRestApiStack extends BaseStack { + + public LambdaRestApiStack(@NotNull final AbstractApp app, @NotNull final Environment environment) { + super(app, environment); + + try { + final String testLambdaCodePath = TestLambdaUtils.getTestLambdaCodePath(Path.of(System.getProperty("java.io.tmpdir"))) + .toFile().getPath(); + + final CustomRuntime2023FunctionProps functionProps = CustomRuntime2023FunctionProps.builder() + .description("Test Function for CDK") + .handler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .code(Code.fromAsset(testLambdaCodePath)) + .build(); + + final CustomRuntime2023Function function = + new CustomRuntime2023Function<>(this, SafeString.of("Function"), functionProps); + + // To test {@link LambdaPermissionAssert} + final LambdaRestApiProps lambdaRestApiProps = LambdaRestApiProps.builder() + .handler(function.getFunction()) + .deployOptions(StageOptions.builder().stageName("Test").build()) + .build(); + + new io.sandpipers.cdk.core.construct.apigateway.LambdaRestApi(this, SafeString.of("LambdaRestApi"), lambdaRestApiProps); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/RestApiStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/RestApiStack.java new file mode 100644 index 0000000..839149e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/RestApiStack.java @@ -0,0 +1,67 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.apigateway.RestApi; +import io.sandpipers.cdk.core.construct.apigateway.RestApi.RestApiProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apigateway.DomainNameOptions; +import software.amazon.awscdk.services.apigateway.Integration; +import software.amazon.awscdk.services.apigateway.IntegrationType; +import software.amazon.awscdk.services.apigateway.Resource; +import software.amazon.awscdk.services.apigateway.StageOptions; +import software.amazon.awscdk.services.certificatemanager.Certificate; + +public class RestApiStack extends BaseStack { + + public RestApiStack(@NotNull final AbstractApp app, @NotNull final Environment environment) { + super(app, environment); + + final Integration integration = Integration.Builder.create() + .integrationHttpMethod("ANY") + .type(IntegrationType.MOCK) + .build(); + + final String domainName = "sandpipers.yeah"; + final Certificate certificate = Certificate.Builder.create(this, "Certificate") + .domainName(domainName) + .build(); + + final DomainNameOptions domainNameOptions = DomainNameOptions.builder() + .domainName(domainName) + .certificate(certificate).build(); + + final RestApiProps restApiProps = RestApiProps.builder() + .deployOptions(StageOptions.builder().stageName("Test").build()) + .defaultIntegration(integration) + .domainName(domainNameOptions) + .build(); + + final RestApi restApi = new RestApi(this, SafeString.of("RestApi"), restApiProps); + + final Resource users = restApi.getRestApi().getRoot().addResource("users"); + users.addMethod("GET"); + users.addMethod("POST"); + + users.addResource("{id}") + .addMethod("GET"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/TestLambdaUtils.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/TestLambdaUtils.java new file mode 100644 index 0000000..a70f2d7 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/main/java/io/sandpipers/cdk/example/apigateway/TestLambdaUtils.java @@ -0,0 +1,45 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TestLambdaUtils { + + public static Path getTestLambdaCodePath(final Path tempDir) throws IOException { + final Path lambdaCodePath = tempDir.resolve("lambda-package.zip"); + + final File file = lambdaCodePath.toFile(); + + if (file.exists()) { + return lambdaCodePath; + } + + final boolean isCreated = file.createNewFile(); + + if (!isCreated) { + throw new IOException("Failed to create lambda package"); + } + return lambdaCodePath; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiTest.java new file mode 100644 index 0000000..014ace2 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/LambdaRestApiTest.java @@ -0,0 +1,191 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.apigateway.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class LambdaRestApiTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + final Application app = new Application(); + + final LambdaRestApiStack lambdaRestApiStack = new LambdaRestApiStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(lambdaRestApiStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(lambdaRestApiStack); + } + + @Test + void should_have_rest_api() { + + assertThat(template) + .containsRestApi("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasTag("APPLICATION_NAME", "apigateway-cdk-example") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST); + } + + @Test + void should_have_rest_api_account() { + assertThat(template) + .containsApiAccount("^LambdaRestApiAccount[a-zA-Z0-9]{8}$") + .hasCloudWatchRole("^LambdaRestApiCloudWatchRole[a-zA-Z0-9]{8}$") + .hasDependency("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasUpdateReplacePolicy("Retain") + .hasDeletionPolicy("Retain"); + } + + @Test + void should_have_rest_api_deployment() { + + assertThat(template) + .containsApiDeployment("^LambdaRestApiDeployment(.*)$") + .hasRestApiId("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasDescription("Automatically created by the RestApi construct") + .hasDependency("^LambdaRestApiproxyANY[a-zA-Z0-9]{8}$") + .hasDependency("^LambdaRestApiproxy[a-zA-Z0-9]{8}$") + .hasDependency("^LambdaRestApiANY[a-zA-Z0-9]{8}$"); + } + + @Test + void should_have_rest_api_stage() { + + assertThat(template) + .containsApiStage("^LambdaRestApiDeploymentStageTest[a-zA-Z0-9]{8}$") + .hasStageName("Test") + .hasRestApiId("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasDeploymentId(("^LambdaRestApiDeployment(.*)$")) + .hasDependency("^LambdaRestApiAccount[a-zA-Z0-9]{8}$") + .hasTag("APPLICATION_NAME", "apigateway-cdk-example") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST); + } + + @Test + void should_have_resource() { + + assertThat(template) + .containsApiResource("^LambdaRestApiproxy[a-zA-Z0-9]{8}$") + .hasPath("{proxy+}") + .hasRestApiId("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasParentId("^LambdaRestApi[a-zA-Z0-9]{8}$"); + } + + @Test + void should_have_methods() { + + final String IntegrationArn = "^arn:aws:apigateway::ap-southeast-2::lambda:path/2015-03-31/functions/Function[a-zA-Z0-9]{8}/invocations$"; + + assertThat(template) + .containsApiMethod("^LambdaRestApiANY[a-zA-Z0-9]{8}$") + .hasHttpMethod("ANY") + .hasIntegration("POST", "AWS_PROXY", IntegrationArn) + .hasAuthorizationType("NONE") + .hasResourceId("^LambdaRestApi[a-zA-Z0-9]{8}$") + .hasRestApiId(("^LambdaRestApi[a-zA-Z0-9]{8}$")); + + assertThat(template) + .containsApiMethod("^LambdaRestApiproxyANY[a-zA-Z0-9]{8}$") + .hasHttpMethod("ANY") + .hasIntegration("POST", "AWS_PROXY", IntegrationArn) + .hasAuthorizationType("NONE") + .hasResourceId("^LambdaRestApiproxy[a-zA-Z0-9]{8}$") + .hasRestApiId(("^LambdaRestApi[a-zA-Z0-9]{8}$")); + } + + @Test + void should_have_role_with_AmazonAPIGatewayPushToCloudWatchLogs_policy_for_rest_api_to_push_logs_to_cloud_watch() { + final Map principal = Map.of("Service", "apigateway.amazonaws.com"); + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + final String managedPolicyArn = ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"; + + // TODO change hasManagedPolicyArn and use the following instead + // final String managedPolicyArn = "arn:aws::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + + assertThat(template) + .containsRole("^LambdaRestApiCloudWatchRole[a-zA-Z0-9]{8}$") + .hasManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, "sts:AssumeRole") + .hasDeletionPolicy("Retain") + .hasUpdateReplacePolicy("Retain") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "apigateway-cdk-example"); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_deployment_stage_test() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:LambdaRestApi[a-zA-Z0-9]{8}/LambdaRestApiDeploymentStageTest[a-zA-Z0-9]{8}/\\*/\\*$"; + final String lambdaPermissionResourceId = "^LambdaRestApiproxyANYApiPermissionSandpipersApigatewayCdkExampleStakeLambdaRestApi[a-zA-Z0-9]{8}ANYproxy[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_test_invoke_stage() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:LambdaRestApi[a-zA-Z0-9]{8}/test-invoke-stage/\\*/\\*$"; + final String lambdaPermissionResourceId = "^LambdaRestApiproxyANYApiPermissionTestSandpipersApigatewayCdkExampleStakeLambdaRestApi[a-zA-Z0-9]{8}ANYproxy[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_deployment_stage_test_without_wildcards() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:LambdaRestApi[a-zA-Z0-9]{8}/LambdaRestApiDeploymentStageTest[a-zA-Z0-9]{8}/\\*/$"; + final String lambdaPermissionResourceId = "^LambdaRestApiANYApiPermissionSandpipersApigatewayCdkExampleStakeLambdaRestApi[a-zA-Z0-9]{8}ANY[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_test_invoke_stage_without_wildcards() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:LambdaRestApi[a-zA-Z0-9]{8}/test-invoke-stage/\\*/$"; + final String lambdaPermissionResourceId = "^LambdaRestApiANYApiPermissionTestSandpipersApigatewayCdkExampleStakeLambdaRestApi[a-zA-Z0-9]{8}ANY[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/RestApiTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/RestApiTest.java new file mode 100644 index 0000000..dcc1fd9 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/RestApiTest.java @@ -0,0 +1,187 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.apigateway.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class RestApiTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + final Application app = new Application(); + + final RestApiStack restApiStack = new RestApiStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(restApiStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(restApiStack); + } + + @Test + void should_have_rest_api() { + + assertThat(template) + .containsRestApi("^RestApi[a-zA-Z0-9]{8}$") + .hasTag("APPLICATION_NAME", "apigateway-cdk-example") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST); + } + + @Test + void should_have_rest_api_account() { + assertThat(template) + .containsApiAccount("^RestApiAccount[a-zA-Z0-9]{8}$") + .hasCloudWatchRole("^RestApiCloudWatchRole[a-zA-Z0-9]{8}$") + .hasDependency("^RestApi[a-zA-Z0-9]{8}$") + .hasUpdateReplacePolicy("Retain") + .hasDeletionPolicy("Retain"); + } + + @Test + void should_have_rest_api_deployment() { + + assertThat(template) + .containsApiDeployment("^RestApiDeployment(.*)$") + .hasRestApiId("^RestApi[a-zA-Z0-9]{8}$") + .hasDescription("Automatically created by the RestApi construct") + .hasDependency("RestApiusersid[a-zA-Z0-9]{8}$") + .hasDependency("RestApiusersid[a-zA-Z0-9]{8}$") + .hasDependency("RestApiusersGET[a-zA-Z0-9]{8}$") + .hasDependency("RestApiusersPOST[a-zA-Z0-9]{8}$") + .hasDependency("RestApiusers[a-zA-Z0-9]{8}$"); + } + + @Test + void should_have_rest_api_stage() { + + assertThat(template) + .containsApiStage("^RestApiDeploymentStageTest[a-zA-Z0-9]{8}$") + .hasStageName("Test") + .hasRestApiId("^RestApi[a-zA-Z0-9]{8}$") + .hasDeploymentId(("^RestApiDeployment(.*)$")) + .hasDependency("^RestApiAccount[a-zA-Z0-9]{8}$") + .hasTag("APPLICATION_NAME", "apigateway-cdk-example") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST); + } + + @Test + void should_have_root_resources() { + + assertThat(template) + .containsApiResource("^RestApiusers[a-zA-Z0-9]{8}$") + .hasPath("users") + .hasRestApiId("^RestApi[a-zA-Z0-9]{8}$") + .hasParentId("^RestApi[a-zA-Z0-9]{8}$"); + + assertThat(template) + .containsApiResource("^RestApiusersid[a-zA-Z0-9]{8}$") + .hasPath("{id}") + .hasRestApiId("^RestApi[a-zA-Z0-9]{8}$") + .hasParentId("^RestApiusers[a-zA-Z0-9]{8}$"); + + } + + @Test + void should_have_methods() { + + assertThat(template) + .containsApiMethod("^RestApiusersGET[a-zA-Z0-9]{8}$") + .hasHttpMethod("GET") + .hasIntegration("ANY", "MOCK", null) + .hasAuthorizationType("NONE") + .hasResourceId("^RestApiusers[a-zA-Z0-9]{8}$") + .hasRestApiId(("^RestApi[a-zA-Z0-9]{8}$")); + + assertThat(template) + .containsApiMethod("^RestApiusersPOST[a-zA-Z0-9]{8}$") + .hasHttpMethod("POST") + .hasIntegration("ANY", "MOCK", null) + .hasAuthorizationType("NONE") + .hasResourceId("^RestApiusers[a-zA-Z0-9]{8}$") + .hasRestApiId(("^RestApi[a-zA-Z0-9]{8}$")); + + assertThat(template) + .containsApiMethod("^RestApiusersidGET[a-zA-Z0-9]{8}$") + .hasHttpMethod("GET") + .hasIntegration("ANY", "MOCK", null) + .hasAuthorizationType("NONE") + .hasResourceId("^RestApiusersid[a-zA-Z0-9]{8}$") + .hasRestApiId(("^RestApi[a-zA-Z0-9]{8}$")); + } + + @Test + void should_have_role_with_AmazonAPIGatewayPushToCloudWatchLogs_policy_for_rest_api_to_push_logs_to_cloud_watch() { + final Map principal = Map.of("Service", "apigateway.amazonaws.com"); + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + final String managedPolicyArn = ":iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs"; + + // TODO change hasManagedPolicyArn and use the following instead + // final String managedPolicyArn = "arn:aws::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs" + + assertThat(template) + .containsRole("^RestApiCloudWatchRole[a-zA-Z0-9]{8}$") + .hasManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, "sts:AssumeRole") + .hasDeletionPolicy("Retain") + .hasUpdateReplacePolicy("Retain") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "apigateway-cdk-example"); + } + + @Test + void should_have_custom_domain_name(){ + assertThat(template) + .containsApiDomainName("^RestApiCustomDomain[a-zA-Z0-9]{8}$") + .hasDomainName("sandpipers.yeah") + .hasRegionalCertificateArn("^Certificate[a-zA-Z0-9]{8}$") + .hasEndpointConfiguration("REGIONAL") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "apigateway-cdk-example"); + } + + @Test + void should_have_certificate(){ + assertThat(template) + .containsCertificate("^Certificate[a-zA-Z0-9]{8}$") + .hasDomainName("sandpipers.yeah") + .hasDomainValidationOptions("sandpipers.yeah", "yeah") + .hasValidationMethod("EMAIL") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "apigateway-cdk-example"); + } + + @Test + void should_have_path_mapping(){ + assertThat(template) + .containsApiPathMapping("^RestApiCustomDomainMapSandpipersApigatewayCdkExampleStakeRestApi(.*)$") + .hasDomainName("^RestApiCustomDomain[a-zA-Z0-9]{8}$") + .hasRestApiId(("^RestApi[a-zA-Z0-9]{8}$")) + .hasStage("^RestApiDeploymentStageTest[a-zA-Z0-9]{8}$"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/TemplateSupport.java new file mode 100644 index 0000000..329680b --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-apigateway/src/test/java/io/sandpipers/cdk/example/apigateway/TemplateSupport.java @@ -0,0 +1,33 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.apigateway; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + + protected static final String TEST = "TEST"; + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/pom.xml new file mode 100644 index 0000000..c8274f8 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-dynamodb + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Application.java new file mode 100644 index 0000000..90e349e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Application.java @@ -0,0 +1,46 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import static io.sandpipers.cdk.example.dynamodb.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("dynamodb-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final DynamoDBStack dynamoDBStack = new DynamoDBStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(dynamoDBStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/CostCentre.java new file mode 100644 index 0000000..ce0c0de --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/DynamoDBStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/DynamoDBStack.java new file mode 100644 index 0000000..68f3505 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/DynamoDBStack.java @@ -0,0 +1,46 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.dynamodb.TableV2; +import io.sandpipers.cdk.core.construct.dynamodb.TableV2.TableProps; +import io.sadpipers.cdk.type.KebabCaseString; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.dynamodb.Attribute; +import software.amazon.awscdk.services.dynamodb.AttributeType; +import software.amazon.awscdk.services.dynamodb.Billing; + +public class DynamoDBStack extends BaseStack { + + public DynamoDBStack(@NotNull final AbstractApp app, @NotNull final Environment environment) { + super(app, environment); + + final TableV2.TableProps tableProps = TableProps.builder() + .billing(Billing.onDemand()) + .partitionKey(Attribute.builder().name("id").type(AttributeType.STRING).build()) + .sortKey(Attribute.builder().name("createdAt").type(AttributeType.NUMBER).build()) + .timeToLiveAttribute("expiresAt") + .tableName(KebabCaseString.of("Secrets")) + .build(); + + new TableV2(this, SafeString.of("Table"), tableProps); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Environment.java new file mode 100644 index 0000000..239e5d5 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/main/java/io/sandpipers/cdk/example/dynamodb/Environment.java @@ -0,0 +1,52 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/DynamoDBTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/DynamoDBTest.java new file mode 100644 index 0000000..0008c1d --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/DynamoDBTest.java @@ -0,0 +1,56 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.dynamodb.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class DynamoDBTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + + final Application app = new Application(); + + final DynamoDBStack dynamoDBStack = new DynamoDBStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + + tagResources(dynamoDBStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(dynamoDBStack); + } + + @Test + void should_have_dynamodb_table() { + assertThat(template) + .containsDynamoDBTable("^Table[a-zA-Z0-9]{8}$") + .hasBillingMode("PAY_PER_REQUEST") + .hasName("secrets") + .hasSSESpecification(false) + .hasKeySchema("id", "HASH") + .hasKeySchema("createdAt", "RANGE") + .hasAttributeDefinitions("id", "S") + .hasAttributeDefinitions("createdAt", "N") + .hasUpdateReplacePolicy("Retain") + .hasDeletionPolicy("Retain"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/TemplateSupport.java new file mode 100644 index 0000000..20a1f3e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-dynamodb/src/test/java/io/sandpipers/cdk/example/dynamodb/TemplateSupport.java @@ -0,0 +1,33 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.dynamodb; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + + protected static final String TEST = "TEST"; + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/pom.xml new file mode 100644 index 0000000..9a430de --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-lambda + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Application.java new file mode 100644 index 0000000..79b7733 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Application.java @@ -0,0 +1,47 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + + +import static io.sandpipers.cdk.example.lambda.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("lambda-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final LambdaStack lambdaStack = new LambdaStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(lambdaStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/CostCentre.java new file mode 100644 index 0000000..68d4903 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Environment.java new file mode 100644 index 0000000..b4a24a4 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/Environment.java @@ -0,0 +1,52 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/LambdaStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/LambdaStack.java new file mode 100644 index 0000000..e58f82e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/LambdaStack.java @@ -0,0 +1,67 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import static io.sandpipers.cdk.example.lambda.TestLambdaUtils.getTestLambdaCodePath; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function; +import io.sandpipers.cdk.core.construct.lambda.CustomRuntime2023Function.CustomRuntime2023FunctionProps; +import io.sadpipers.cdk.type.SafeString; +import java.io.IOException; +import java.nio.file.Path; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.apigateway.LambdaRestApiProps; +import software.amazon.awscdk.services.apigateway.StageOptions; +import software.amazon.awscdk.services.lambda.Code; + +public class LambdaStack extends BaseStack { + + public LambdaStack(@NotNull final AbstractApp app, @NotNull final Environment environment) { + super(app, environment); + + try { + final String testLambdaCodePath = getTestLambdaCodePath(Path.of(System.getProperty("java.io.tmpdir"))) + .toFile().getPath(); + + final CustomRuntime2023FunctionProps functionProps = CustomRuntime2023FunctionProps.builder() + .description("Test Function for CDK") + .handler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .code(Code.fromAsset(testLambdaCodePath)) + .deadLetterTopicEnabled(true) + .environment(Map.of("ENV", "TEST", "SPRING_PROFILES_ACTIVE", "TEST")) + .build(); + + final CustomRuntime2023Function function = + new CustomRuntime2023Function<>(this, SafeString.of("Function"), functionProps); + + // To test {@link LambdaPermissionAssert} + final LambdaRestApiProps restApiProps = LambdaRestApiProps.builder() + .handler(function.getFunction()) + .deployOptions(StageOptions.builder().stageName("Test").build()) + .build(); + + new io.sandpipers.cdk.core.construct.apigateway.LambdaRestApi(this, SafeString.of("RestApi"), restApiProps); + + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/TestLambdaUtils.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/TestLambdaUtils.java new file mode 100644 index 0000000..af06887 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/main/java/io/sandpipers/cdk/example/lambda/TestLambdaUtils.java @@ -0,0 +1,45 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public final class TestLambdaUtils { + + public static Path getTestLambdaCodePath(final Path tempDir) throws IOException { + final Path lambdaCodePath = tempDir.resolve("lambda-package.zip"); + + final File file = lambdaCodePath.toFile(); + + if (file.exists()) { + return lambdaCodePath; + } + + final boolean isCreated = file.createNewFile(); + + if (!isCreated) { + throw new IOException("Failed to create lambda package"); + } + return lambdaCodePath; + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/LambdaTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/LambdaTest.java new file mode 100644 index 0000000..96330aa --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/LambdaTest.java @@ -0,0 +1,164 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.lambda.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class LambdaTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + + final Application app = new Application(); + + final LambdaStack lambdaStack = new LambdaStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(lambdaStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(lambdaStack); + } + + @Test + void should_have_lambda_function() { + + assertThat(template) + .containsFunction("^Function[a-zA-Z0-9]{8}$") + .hasHandler("org.springframework.cloud.function.adapter.aws.FunctionInvoker::handleRequest") + .hasCode("^cdk-sandpipers-assets-ap-southeast-2-(.*)$", "(.*).zip") + .hasRole("^FunctionServiceRole[a-zA-Z0-9]{8}$") + .hasDependency("^FunctionServiceRoleDefaultPolicy[a-zA-Z0-9]{8}$") + .hasDependency("^FunctionServiceRole[a-zA-Z0-9]{8}$") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "lambda-cdk-example") + .hasEnvironmentVariable("ENV", TEST) + .hasEnvironmentVariable("SPRING_PROFILES_ACTIVE", TEST) + .hasDescription("Test Function for CDK") + .hasMemorySize(512) + .hasRuntime("provided.al2023") + .hasTimeout(10) + .hasMemorySize(512) + .hasDeadLetterTarget("^FunctionDeadLetterTopic[a-zA-Z0-9]{8}$"); + } + + @Test + void should_have_default_policy_to_allow_lambda_publish_to_sns() { + + final String policyName = "^FunctionServiceRoleDefaultPolicy[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsPolicy(policyName) + .hasName(policyName) + .isAssociatedWithRole("^FunctionServiceRole[a-zA-Z0-9]{8}$") + .hasPolicyDocumentStatement(null, + "^FunctionDeadLetterTopic[a-zA-Z0-9]{8}$", + "sns:Publish", + "Allow", + "2012-10-17"); + } + + @Test + void should_have_event_invoke_config() { + + assertThat(template) + .containsLambdaEventInvokeConfig("^FunctionEventInvokeConfig[a-zA-Z0-9]{8}$") + .hasLambdaEventInvokeConfig("^Function[a-zA-Z0-9]{8}$", null, null) + .hasQualifier("$LATEST") + .hasFunctionName("^Function[a-zA-Z0-9]{8}$") + .hasMaximumRetryAttempts(2) + .hasMaximumEventAgeInSeconds(60); + } + + @Test + void should_have_service_role_with_AWSLambdaBasicExecutionRole_policy_to_assume_by_lambda() { + final Map principal = Map.of("Service", "lambda.amazonaws.com"); + final String effect = "Allow"; + final String policyDocumentVersion = "2012-10-17"; + final String managedPolicyArn = ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"; + + // TODO change hasManagedPolicyArn and use the following instead + // final String managedPolicyArn = "arn:aws::iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + + assertThat(template) + .containsRole("^FunctionServiceRole[a-zA-Z0-9]{8}$") + .hasManagedPolicyArn(managedPolicyArn) + .hasAssumeRolePolicyDocument(principal, null, effect, policyDocumentVersion, "sts:AssumeRole") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "lambda-cdk-example"); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_deployment_stage_test() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:RestApi[a-zA-Z0-9]{8}/RestApiDeploymentStageTest[a-zA-Z0-9]{8}/\\*/\\*$"; + final String lambdaPermissionResourceId = "^RestApiproxyANYApiPermissionSandpipersLambdaCdkExampleStakeRestApi[a-zA-Z0-9]{8}ANYproxy[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_test_invoke_stage() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:RestApi[a-zA-Z0-9]{8}/test-invoke-stage/\\*/\\*$"; + final String lambdaPermissionResourceId = "^RestApiproxyANYApiPermissionTestSandpipersLambdaCdkExampleStakeRestApi[a-zA-Z0-9]{8}ANYproxy[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission( + lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_deployment_stage_test_without_wildcards() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:RestApi[a-zA-Z0-9]{8}/RestApiDeploymentStageTest[a-zA-Z0-9]{8}/\\*/$"; + final String lambdaPermissionResourceId = "^RestApiANYApiPermissionSandpipersLambdaCdkExampleStakeRestApi[a-zA-Z0-9]{8}ANY[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } + + @Test + void should_have_permission_to_allow_rest_api_call_lambda_from_test_invoke_stage_without_wildcards() { + final String action = "lambda:InvokeFunction"; + final String principal = "apigateway.amazonaws.com"; + final String functionName = "^Function[a-zA-Z0-9]{8}$"; + final String sourceArnPattern = "^arn:aws:execute-api::ap-southeast-2:RestApi[a-zA-Z0-9]{8}/test-invoke-stage/\\*/$"; + final String lambdaPermissionResourceId = "^RestApiANYApiPermissionTestSandpipersLambdaCdkExampleStakeRestApi[a-zA-Z0-9]{8}ANY[a-zA-Z0-9]{8}$"; + + assertThat(template) + .containsLambdaPermission(lambdaPermissionResourceId) + .hasLambdaPermission(functionName, action, principal, sourceArnPattern); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/TemplateSupport.java new file mode 100644 index 0000000..99172f9 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-lambda/src/test/java/io/sandpipers/cdk/example/lambda/TemplateSupport.java @@ -0,0 +1,33 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.lambda; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + + protected static final String TEST = "TEST"; + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/pom.xml new file mode 100644 index 0000000..63c96a4 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-route53 + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Application.java new file mode 100644 index 0000000..d370985 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Application.java @@ -0,0 +1,47 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + + +import static io.sandpipers.cdk.example.route53.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("route53-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final Route53Stack route53Stack = new Route53Stack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(route53Stack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/CostCentre.java new file mode 100644 index 0000000..09174a3 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Environment.java new file mode 100644 index 0000000..a7cf4af --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Environment.java @@ -0,0 +1,52 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Route53Stack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Route53Stack.java new file mode 100644 index 0000000..34a3195 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/main/java/io/sandpipers/cdk/example/route53/Route53Stack.java @@ -0,0 +1,71 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.ec2.Vpc; +import io.sandpipers.cdk.core.construct.ec2.Vpc.VpcProps; +import io.sandpipers.cdk.core.construct.route53.PrivateARecord; +import io.sandpipers.cdk.core.construct.route53.PrivateARecord.PrivateARecordProps; +import io.sandpipers.cdk.core.construct.route53.PublicARecord; +import io.sandpipers.cdk.core.construct.route53.PublicARecord.PublicARecordProps; +import io.sadpipers.cdk.type.IPv4Cidr; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.route53.HostedZone; +import software.amazon.awscdk.services.route53.RecordTarget; + +public class Route53Stack extends BaseStack { + + public Route53Stack(@NotNull final AbstractApp app, @NotNull final Environment environment) { + super(app, environment); + + final VpcProps vpcProps = VpcProps.builder() + .iPv4Cidr(IPv4Cidr.of("192.168.1.0/24")) + .natGateways(0) + .build(); + + Vpc vpc = new Vpc(this, SafeString.of("Vpc"), vpcProps); + + final PublicARecordProps publicARecordProps = PublicARecordProps.builder() + .recordName(app.getApplicationName().getValue()) + .domainName("services.sandpipers.yeah") + .vpcId(vpc.getVpc().getVpcId()) + .target(RecordTarget.fromValues("services.load.balancer")) + .zone(getPublicHostedZone("PublicHostedZone")) + .build(); + + new PublicARecord<>(this, SafeString.of("PublicARecord"), publicARecordProps); + + final PrivateARecordProps privateARecordProps = PrivateARecordProps.builder() + .recordName(app.getApplicationName().getValue()) + .domainName("internal.services.sandpipers.yeah") + .vpcId(vpc.getVpc().getVpcId()) + .target(RecordTarget.fromValues("services.load.balancer")) + .zone(getPublicHostedZone("PrivateHostedZone")) + .build(); + new PrivateARecord<>(this, SafeString.of("PrivateARecord"), privateARecordProps); + } + + private HostedZone getPublicHostedZone(final String hostedZoneId) { + return HostedZone.Builder.create(this, hostedZoneId) + .zoneName("sandpipers.yeah") + .build(); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/Route53Test.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/Route53Test.java new file mode 100644 index 0000000..1584f30 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/Route53Test.java @@ -0,0 +1,87 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.route53.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import java.util.List; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class Route53Test extends TemplateSupport { + + @BeforeAll + static void initAll() { + + final Application app = new Application(); + + final Route53Stack route53Stack = new Route53Stack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(route53Stack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(route53Stack); + } + + @Test + void should_have_public_a_record() { + assertThat(template) + .containsRecordSet("^PublicARecord[a-zA-Z0-9]{8}$") + .hasName("route53-cdk-example.sandpipers.yeah.") + .hasHostedZone("^PublicHostedZone[a-zA-Z0-9]{8}$") + .hasResourceRecords(List.of("services.load.balancer")) + .hasType("A") + .hasTtl("1800") + .hasUpdateReplacePolicy("Delete") + .hasDeletionPolicy("Delete"); + } + + @Test + void should_have_private_a_record() { + assertThat(template) + .containsRecordSet("^PrivateARecord[a-zA-Z0-9]{8}$") + .hasName("route53-cdk-example.sandpipers.yeah.") + .hasHostedZone("^PrivateHostedZone[a-zA-Z0-9]{8}$") + .hasResourceRecords(List.of("services.load.balancer")) + .hasType("A") + .hasTtl("1800") + .hasUpdateReplacePolicy("Delete") + .hasDeletionPolicy("Delete"); + } + + @Test + void should_have_public_a_hosted_zone() { + assertThat(template) + .containsHostedZone("^PublicHostedZone[a-zA-Z0-9]{8}$") + .hasName("sandpipers.yeah.") + .hasTag("APPLICATION_NAME", "route53-cdk-example") + .hasTag("ENVIRONMENT", "TEST") + .hasTag("COST_CENTRE", "Sandpipers"); + } + + @Test + void should_have_private_a_hosted_zone() { + assertThat(template) + .containsHostedZone("^PrivateHostedZone[a-zA-Z0-9]{8}$") + .hasName("sandpipers.yeah.") + .hasTag("APPLICATION_NAME", "route53-cdk-example") + .hasTag("ENVIRONMENT", "TEST") + .hasTag("COST_CENTRE", "Sandpipers"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/TemplateSupport.java new file mode 100644 index 0000000..9bd841e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-route53/src/test/java/io/sandpipers/cdk/example/route53/TemplateSupport.java @@ -0,0 +1,31 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.route53; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/pom.xml new file mode 100644 index 0000000..1169a5a --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-sns + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Application.java new file mode 100644 index 0000000..90ba43d --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Application.java @@ -0,0 +1,50 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + + +import static io.sandpipers.cdk.example.sns.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("sns-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final TopicStack topicStack = new TopicStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(topicStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + final FifoTopicStack fifoTopicStack = new FifoTopicStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(fifoTopicStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/CostCentre.java new file mode 100644 index 0000000..5392191 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Environment.java new file mode 100644 index 0000000..78fbe0e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/Environment.java @@ -0,0 +1,52 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/FifoTopicStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/FifoTopicStack.java new file mode 100644 index 0000000..afc71ba --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/FifoTopicStack.java @@ -0,0 +1,60 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.sns.FifoTopic; +import io.sandpipers.cdk.core.construct.sns.FifoTopic.FifoTopicProps; +import io.sandpipers.cdk.core.construct.sqs.Queue; +import io.sandpipers.cdk.core.construct.sqs.Queue.QueueProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.kms.IKey; +import software.amazon.awscdk.services.kms.Key; +import software.amazon.awscdk.services.sns.Subscription; +import software.amazon.awscdk.services.sns.SubscriptionProtocol; + +public class FifoTopicStack extends BaseStack { + + public FifoTopicStack(@NotNull AbstractApp app, @NotNull Environment environment) { + super(app, environment); + + final IKey key = Key.Builder.create(this, "Key") + .description("Key for the topic") + .build(); + + final FifoTopicProps fifoTopicProps = FifoTopicProps.builder() + .contentBasedDeduplication(true) + .messageRetentionPeriodInDays(30) + .masterKey(key) + .build(); + + final FifoTopic fifoTopic = new FifoTopic<>(this, SafeString.of("Topic"), fifoTopicProps); + + final QueueProps deadLetterQueueProps = QueueProps.builder().requireDeadLetterQueue(false).build(); + final Queue deadLetterQueue = new Queue<>(this, SafeString.of("DeadLetterQueue"), deadLetterQueueProps); + + Subscription.Builder.create(this, "Subscription") + .topic(fifoTopic.getTopic()) + .deadLetterQueue(deadLetterQueue.getQueue()) + .protocol(SubscriptionProtocol.SQS) + .endpoint("http://example.com") + .build(); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/TopicStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/TopicStack.java new file mode 100644 index 0000000..b4fc80e --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/main/java/io/sandpipers/cdk/example/sns/TopicStack.java @@ -0,0 +1,58 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.sns.Topic; +import io.sandpipers.cdk.core.construct.sns.Topic.TopicProps; +import io.sandpipers.cdk.core.construct.sqs.Queue; +import io.sandpipers.cdk.core.construct.sqs.Queue.QueueProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.kms.IKey; +import software.amazon.awscdk.services.kms.Key; +import software.amazon.awscdk.services.sns.Subscription; +import software.amazon.awscdk.services.sns.SubscriptionProtocol; + +public class TopicStack extends BaseStack { + + public TopicStack(@NotNull AbstractApp app, @NotNull Environment environment) { + super(app, environment); + + final IKey key = Key.Builder.create(this, "Key") + .description("Key for the topic") + .build(); + + final TopicProps topicProps = TopicProps.builder() + .masterKey(key) + .build(); + + final Topic topic = new Topic<>(this, SafeString.of("Topic"), topicProps); + + final QueueProps deadLetterQueueProps = QueueProps.builder().requireDeadLetterQueue(false).build(); + final Queue deadLetterQueue = new Queue<>(this, SafeString.of("DeadLetterQueue"), deadLetterQueueProps); + + Subscription.Builder.create(this, "Subscription") + .topic(topic.getTopic()) + .endpoint("http://example.com") + .deadLetterQueue(deadLetterQueue.getQueue()) + .protocol(SubscriptionProtocol.HTTP) + .build(); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/FifoTopicTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/FifoTopicTest.java new file mode 100644 index 0000000..1c3eb3d --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/FifoTopicTest.java @@ -0,0 +1,65 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.sns.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class FifoTopicTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + + final Application app = new Application(); + + final FifoTopicStack fifoTopicStack = new FifoTopicStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(fifoTopicStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(fifoTopicStack); + } + + @Test + void should_have_topic() { + + assertThat(template) + .containsTopic("^Topic[a-zA-Z0-9]{8}$") + .hasTopicName("^SandpipersSnsCdkExampleStake-Topic-[a-zA-Z0-9]{8}.fifo$") + .isFifo(true) + .hasContentBasedDeduplicationEnabled(true) + .hasMessageRetentionPeriodInDays(30) + .hasMasterKey("^Key[a-zA-Z0-9]{8}$") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sns-cdk-example"); + } + + @Test + void should_have_topic_http_subscription_with_dead_letter_topic() { + assertThat(template) + .containsTopicSubscription("^Subscription[a-zA-Z0-9]{8}$") + .hasTopicArn("^Topic[a-zA-Z0-9]{8}$") + .hasProtocol("sqs") + .hasEndpoint("http://example.com") + .hasDeadLetterQueue("^DeadLetterQueue[a-zA-Z0-9]{8}$"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TemplateSupport.java new file mode 100644 index 0000000..04a6633 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TemplateSupport.java @@ -0,0 +1,33 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + + protected static final String TEST = "TEST"; + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TopicTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TopicTest.java new file mode 100644 index 0000000..22a3c01 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sns/src/test/java/io/sandpipers/cdk/example/sns/TopicTest.java @@ -0,0 +1,63 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sns; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.sns.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class TopicTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + final Application app = new Application(); + + final TopicStack topicStack = new TopicStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + + tagResources(topicStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(topicStack); + } + + @Test + void should_have_topic() { + assertThat(template) + .containsTopic("^Topic[a-zA-Z0-9]{8}$") + .isFifo(false) + .hasMasterKey("^Key[a-zA-Z0-9]{8}$") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sns-cdk-example"); + } + + @Test + void should_have_topic_http_subscription_with_dead_letter_topic() { + assertThat(template) + .containsTopicSubscription("^Subscription[a-zA-Z0-9]{8}$") + .hasTopicArn("^Topic[a-zA-Z0-9]{8}$") + .hasProtocol("http") + .hasEndpoint("http://example.com") + .hasDeadLetterQueue("^DeadLetterQueue[a-zA-Z0-9]{8}$"); + } + + // TODO add test for Key +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/pom.xml b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/pom.xml new file mode 100644 index 0000000..f0865ea --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/pom.xml @@ -0,0 +1,78 @@ + + + + + 4.0.0 + + io.sandpipers + sandpipers-cdk-examples + 1.0-SNAPSHOT + + + sandpipers-cdk-example-sqs + + + 21 + 21 + UTF-8 + + + + + io.sandpipers + sandpipers-cdk-constructs + + + + + org.projectlombok + lombok + provided + + + + + + io.sandpipers + sandpipers-cdk-assertions + test + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-core + test + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Application.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Application.java new file mode 100644 index 0000000..250351a --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Application.java @@ -0,0 +1,49 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import static io.sandpipers.cdk.example.sqs.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sadpipers.cdk.type.SafeString; +import lombok.AllArgsConstructor; +import org.jetbrains.annotations.NotNull; + +@AllArgsConstructor +public class Application extends AbstractApp { + + private static final SafeString APPLICATION_NAME = SafeString.of("sqs-cdk-example"); + + public static void main(String[] args) { + final Application app = new Application(); + + final QueueStack queueStack = new QueueStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(queueStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + final QueueStack fifoQueueStack = new QueueStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(fifoQueueStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, APPLICATION_NAME); + + app.synth(); + } + + @NotNull + @Override + public SafeString getApplicationName() { + return SafeString.of(APPLICATION_NAME); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/CostCentre.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/CostCentre.java new file mode 100644 index 0000000..469d123 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/CostCentre.java @@ -0,0 +1,36 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import io.sandpipers.cdk.core.AbstractCostCentre; +import io.sadpipers.cdk.type.AlphanumericString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class CostCentre extends AbstractCostCentre { + + public static final CostCentre SANDPIPERS = CostCentre.builder() + .value(AlphanumericString.of("sandpipers")) + .build(); + + static { + registerCostCentre(SANDPIPERS); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Environment.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Environment.java new file mode 100644 index 0000000..a40e4f7 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/Environment.java @@ -0,0 +1,52 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import static io.sandpipers.cdk.core.util.Constants.AWS_REGION_AP_SOUTHEAST_2; + +import io.sandpipers.cdk.core.AbstractEnvironment; +import io.sadpipers.cdk.type.AWSAccount; +import io.sadpipers.cdk.type.SafeString; +import lombok.Getter; +import lombok.experimental.SuperBuilder; + +@Getter +@SuperBuilder +public class Environment extends AbstractEnvironment { + + public static final Environment SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + + static { + final AWSAccount awsAccount = AWSAccount.of("111111111111"); + final SafeString awsRegion = SafeString.of(AWS_REGION_AP_SOUTHEAST_2); + + final software.amazon.awscdk.Environment awsEnvironment = software.amazon.awscdk.Environment.builder() + .account(awsAccount.getValue()) + .account(awsRegion.getValue()) + .build(); + + SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2 = Environment.builder() + .awsEnvironment(awsEnvironment) + .costCentre(CostCentre.SANDPIPERS) + .environmentName(SafeString.of("TEST")) + .environmentKey(SafeString.of("SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2")) + .build(); + + registerEnvironment(SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + } +} \ No newline at end of file diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/FifoQueueStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/FifoQueueStack.java new file mode 100644 index 0000000..a329243 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/FifoQueueStack.java @@ -0,0 +1,42 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import static software.amazon.awscdk.services.sqs.RedrivePermission.DENY_ALL; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.sqs.FifoQueue; +import io.sandpipers.cdk.core.construct.sqs.FifoQueue.FifoQueueProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; +import software.amazon.awscdk.services.sqs.RedriveAllowPolicy; + +public class FifoQueueStack extends BaseStack { + + public FifoQueueStack(@NotNull AbstractApp app, @NotNull Environment environment) { + super(app, environment); + + final FifoQueueProps fifoQueueProps = FifoQueueProps.builder() + .deadLetterQueueMaxReceiveCount(5) + .redriveAllowPolicy(RedriveAllowPolicy.builder().redrivePermission(DENY_ALL).build()) + .build(); + + new FifoQueue<>(this, SafeString.of("Queue"), fifoQueueProps); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/QueueStack.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/QueueStack.java new file mode 100644 index 0000000..7460824 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/main/java/io/sandpipers/cdk/example/sqs/QueueStack.java @@ -0,0 +1,38 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import io.sandpipers.cdk.core.AbstractApp; +import io.sandpipers.cdk.core.construct.BaseStack; +import io.sandpipers.cdk.core.construct.sqs.Queue; +import io.sandpipers.cdk.core.construct.sqs.Queue.QueueProps; +import io.sadpipers.cdk.type.SafeString; +import org.jetbrains.annotations.NotNull; + +public class QueueStack extends BaseStack { + + public QueueStack(@NotNull AbstractApp app, @NotNull Environment environment) { + super(app, environment); + + final QueueProps queueProps = QueueProps.builder() + .deadLetterQueueMaxReceiveCount(12) + .build(); + + new Queue<>(this, SafeString.of("Queue"), queueProps); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/FifoQueueTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/FifoQueueTest.java new file mode 100644 index 0000000..9bd0f06 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/FifoQueueTest.java @@ -0,0 +1,70 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.sqs.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class FifoQueueTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + final Application app = new Application(); + + final FifoQueueStack fifoQueueStack = new FifoQueueStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + + tagResources(fifoQueueStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(fifoQueueStack); + } + + @Test + void should_have_queue_with_dead_letter_queue() { + assertThat(template) + .containsQueue("^Queue[a-zA-Z0-9]{8}$") + .hasDeadLetterQueue("^QueueDeadLetterQueue[a-zA-Z0-9]{8}$") + .isFifo(true) + .hasContentBasedDeduplicationEnabled(true) + .hasDeduplicationScope("messageGroup") + .hasFifoThroughputLimit("perMessageGroupId") + .hasMaxRetrialCount(5) + .hasUpdateReplacePolicy("Retain") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sqs-cdk-example"); + } + + @Test + void should_have_dead_letter_queue() { + assertThat(template) + .containsQueue("^QueueDeadLetterQueue[a-zA-Z0-9]{8}$") + .isFifo(true) + .hasContentBasedDeduplicationEnabled(true) + .hasDeduplicationScope("messageGroup") + .hasFifoThroughputLimit("perMessageGroupId") + .hasUpdateReplacePolicy("Retain") + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sqs-cdk-example"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/QueueTest.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/QueueTest.java new file mode 100644 index 0000000..f7c0163 --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/QueueTest.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import static io.sandpipers.cdk.assertion.CDKStackAssert.assertThat; +import static io.sandpipers.cdk.core.AbstractApp.tagResources; +import static io.sandpipers.cdk.example.sqs.Environment.SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import software.amazon.awscdk.assertions.Template; + +public class QueueTest extends TemplateSupport { + + @BeforeAll + static void initAll() { + + final Application app = new Application(); + + final QueueStack queueStack = new QueueStack(app, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2); + tagResources(queueStack, SANDPIPERS_TEST_111111111111_AP_SOUTHEAST_2, app.getApplicationName()); + + template = Template.fromStack(queueStack); + } + + @Test + void should_have_queue_with_dead_letter_queue() { + assertThat(template) + .containsQueue("^Queue[a-zA-Z0-9]{8}$") + .hasDeadLetterQueue("^QueueDeadLetterQueue[a-zA-Z0-9]{8}$") + .hasMaxRetrialCount(12) + .hasUpdateReplacePolicy("Retain") + .isFifo(false) + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sqs-cdk-example"); + } + + @Test + void should_have_dead_letter_queue() { + assertThat(template) + .containsQueue("^QueueDeadLetterQueue[a-zA-Z0-9]{8}$") + .hasUpdateReplacePolicy("Retain") + .isFifo(false) + .hasTag("COST_CENTRE", "Sandpipers") + .hasTag("ENVIRONMENT", TEST) + .hasTag("APPLICATION_NAME", "sqs-cdk-example"); + } +} diff --git a/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/TemplateSupport.java b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/TemplateSupport.java new file mode 100644 index 0000000..ca3019a --- /dev/null +++ b/sandpipers-cdk-examples/sandpipers-cdk-example-sqs/src/test/java/io/sandpipers/cdk/example/sqs/TemplateSupport.java @@ -0,0 +1,32 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sandpipers.cdk.example.sqs; + +import org.junit.jupiter.api.AfterAll; +import software.amazon.awscdk.assertions.Template; + +public abstract class TemplateSupport { + protected static final String TEST = "TEST"; + + protected static Template template; + + @AfterAll + static void cleanup() { + template = null; + } +} diff --git a/sandpipers-cdk-types/pom.xml b/sandpipers-cdk-types/pom.xml new file mode 100644 index 0000000..d3a5186 --- /dev/null +++ b/sandpipers-cdk-types/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + + io.sandpipers + sandpipers-cdk + 1.0-SNAPSHOT + + + sandpipers-cdk-types + + + 21 + 21 + UTF-8 + + + + + + org.typefactory + type-factory-core + + + + com.fasterxml.jackson.core + jackson-annotations + + + + + + org.assertj + assertj-core + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.mockito + mockito-junit-jupiter + test + + + + + + \ No newline at end of file diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSAccount.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSAccount.java new file mode 100644 index 0000000..3fbd5cd --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSAccount.java @@ -0,0 +1,61 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [0-9]{12}} and is not blank. Empty values are converted to null. + */ +public class AWSAccount extends StringType implements CharSequence { + + private static final int MIN_SIZE = 12; + private static final int MAX_SIZE = 12; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.aws.account", + "must be a %d digits".formatted(MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptDigits0to9() + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AWSAccount of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AWSAccount::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private AWSAccount(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSArn.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSArn.java new file mode 100644 index 0000000..b69d7bc --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSArn.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class AWSArn extends StringType implements CharSequence { + + private static final int MIN_SIZE = 44; + private static final int MAX_SIZE = 1011; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.aws.arn", + "must be a %d-%d character(s) value containing Alphanumeric as well as '.:_-'".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .matchesRegex(Pattern.compile("^arn:aws(-[\\w]+)*:[a-z0-9-\\\\.]{0,63}:[a-z0-9-\\\\.]{0,63}:[0-9]{12}:(\\w|\\/|-){1,1011}$")) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AWSArn of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AWSArn::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private AWSArn(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSEcrImageIdentifier.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSEcrImageIdentifier.java new file mode 100644 index 0000000..51b3033 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AWSEcrImageIdentifier.java @@ -0,0 +1,65 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class AWSEcrImageIdentifier extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 1024; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.aws.ecr.image.identifier", + "must be a %d-%d character(s) value matching '^([0-9]{12}.dkr.ecr.[a-z\\-]+-[0-9]{1}.amazonaws.com\\/.*)|(^public\\.ecr\\.aws\\/.+\\/.+)$'".formatted( + MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .matchesRegex(Pattern.compile("^([0-9]{12}.dkr.ecr.[a-z\\-]+-[0-9]{1}.amazonaws.com\\/.*)|(^public\\.ecr\\.aws\\/.+\\/.+)$")) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AWSEcrImageIdentifier of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AWSEcrImageIdentifier::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private AWSEcrImageIdentifier(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AlphanumericString.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AlphanumericString.java new file mode 100644 index 0000000..f9e47b1 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AlphanumericString.java @@ -0,0 +1,62 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class AlphanumericString extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.string.alphanumeric", + "must be a %d-%d character(s) value containing Alphanumeric".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AlphanumericString of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AlphanumericString::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private AlphanumericString(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerCpu.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerCpu.java new file mode 100644 index 0000000..65d4da0 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerCpu.java @@ -0,0 +1,62 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +public class AppRunnerCpu extends StringType implements CharSequence { + + private static final int MIN_SIZE = 3; + private static final int MAX_SIZE = 9; + private static final String SPECIAL_CHARS = "."; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.apprunner.cpu", + "must be a %d-%d character(s) value matching '^(?(0\\.25|0\\.5|1|2|4)) vCPU$'".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .matchesRegex(Pattern.compile("^(?(0\\.25|0\\.5|1|2|4))$")) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AppRunnerCpu of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AppRunnerCpu::new); + } + + @JsonValue + public String getValue() { + return "%s vCPU".formatted(value()); + } + + private AppRunnerCpu(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerMemory.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerMemory.java new file mode 100644 index 0000000..5c3afb5 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/AppRunnerMemory.java @@ -0,0 +1,65 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class AppRunnerMemory extends StringType implements CharSequence { + + private static final int MIN_SIZE = 3; + private static final int MAX_SIZE = 6; + private static final String SPECIAL_CHARS = "."; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.apprunner.memory", + "must be a %d-%d character(s) value matching '^(?(0\\.5|1|2|3|4|6|8|10|12)) GB$'".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .matchesRegex(Pattern.compile("^(?(0\\.5|1|2|3|4|6|8|10|12))$")) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static AppRunnerMemory of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, AppRunnerMemory::new); + } + + @JsonValue + public String getValue() { + return "%s GB".formatted(value()); + } + + private AppRunnerMemory(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/IPv4Cidr.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/IPv4Cidr.java new file mode 100644 index 0000000..ce32fc5 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/IPv4Cidr.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String representing cidr block. + */ +public class IPv4Cidr extends StringType implements CharSequence { + private static final int MIN_SIZE = 7; + private static final int MAX_SIZE = 18; + private static final String SPECIAL_CHARS = "./"; + + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.cidr", + "must be a %d-%d character(s) value containing Alphanumeric as well as './".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .matchesRegex(Pattern.compile("^(([0-9]|[1-9][0-9]|1[0-9]{2}+|2[0-4][0-9]|25[0-5])\\.){3}+([0-9]|[1-9][0-9]|1[0-9]{2}+|2[0-4][0-9]|25[0-5])(\\/(\\d|[1-2]\\d|3[0-2]))$")) + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static IPv4Cidr of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, IPv4Cidr::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private IPv4Cidr(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/KebabCaseString.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/KebabCaseString.java new file mode 100644 index 0000000..17b6c16 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/KebabCaseString.java @@ -0,0 +1,65 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class KebabCaseString extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final String SPECIAL_CHARS = "-"; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.string.kebab.case", + "must be a %d-%d character(s) value containing Alphanumeric as well as '-'".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .toLowerCase() + .build(); + + @JsonCreator + public static KebabCaseString of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, KebabCaseString::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private KebabCaseString(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/NumericString.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/NumericString.java new file mode 100644 index 0000000..1c029aa --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/NumericString.java @@ -0,0 +1,61 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class NumericString extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.string.numeric", + "must be a %d-%d character(s) value containing only numbers".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptDigits0to9() + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static NumericString of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, NumericString::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private NumericString(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Path.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Path.java new file mode 100644 index 0000000..b330098 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Path.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9/]} and is not blank. Empty values are converted to null. + */ +public class Path extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final String SPECIAL_CHARS = "/"; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.path", + "must be a %d-%d character(s) value containing Alphanumeric as well as '/".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static Path of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, Path::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private Path(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Protocol.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Protocol.java new file mode 100644 index 0000000..b9a27f1 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/Protocol.java @@ -0,0 +1,63 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import java.util.regex.Pattern; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9/]} and is not blank. Empty values are converted to null. + */ +public class Protocol extends StringType implements CharSequence { + + private static final int MIN_SIZE = 3; + private static final int MAX_SIZE = 4; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.protocol", + "must be a %d-%d character(s) value containing Alphanumeric as well as '/".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .matchesRegex(Pattern.compile("^(?TCP|HTTP)$")) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static Protocol of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, Protocol::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private Protocol(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeString.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeString.java new file mode 100644 index 0000000..5f54d86 --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeString.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class SafeString extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final String SPECIAL_CHARS = ".:_-"; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.string.safe", + "must be a %d-%d character(s) value containing Alphanumeric as well as '.:_-'".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .preserveCase() + .forbidWhitespace() + .convertEmptyToNull() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static SafeString of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, SafeString::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private SafeString(final String value) { + super(value); + } +} diff --git a/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeStringWithWhitespace.java b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeStringWithWhitespace.java new file mode 100644 index 0000000..156eb9a --- /dev/null +++ b/sandpipers-cdk-types/src/main/java/io/sadpipers/cdk/type/SafeStringWithWhitespace.java @@ -0,0 +1,64 @@ +/* + * Licensed to Muhammad Hamadto + * + * 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 + * + * See the NOTICE file distributed with this work for additional information regarding copyright ownership. + * + * 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 io.sadpipers.cdk.type; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import org.typefactory.MessageCode; +import org.typefactory.StringType; +import org.typefactory.TypeParser; + +/** + * A String that can only contain chars {@code [a-zA-Z0-9]} and is not blank. Empty values are converted to null. + */ +public class SafeStringWithWhitespace extends StringType implements CharSequence { + + private static final int MIN_SIZE = 1; + private static final int MAX_SIZE = 500; + private static final String SPECIAL_CHARS = ".:_-"; + private static final MessageCode ERROR_MESSAGE = MessageCode.of( + "invalid.string.safe.whitespace", + "must be a %d-%d character(s) value containing Alphanumeric as well as '.:_-' and whitespaces".formatted(MIN_SIZE, MAX_SIZE) + ); + + private static final TypeParser TYPE_PARSER = TypeParser.builder() + .messageCode(ERROR_MESSAGE) + .acceptLettersAtoZ() + .acceptDigits0to9() + .acceptChars(SPECIAL_CHARS.toCharArray()) + .preserveCase() + .convertEmptyToNull() + .normalizeWhitespace() + .minSize(MIN_SIZE) + .maxSize(MAX_SIZE) + .build(); + + @JsonCreator + public static SafeStringWithWhitespace of(final CharSequence value) { + return TYPE_PARSER.parseToStringType(value, SafeStringWithWhitespace::new); + } + + @JsonValue + public String getValue() { + return value(); + } + + private SafeStringWithWhitespace(final String value) { + super(value); + } +}