diff --git a/.github/actions/application-deploy/action.yml b/.github/actions/application-deploy/action.yml new file mode 100644 index 0000000..ebecc0d --- /dev/null +++ b/.github/actions/application-deploy/action.yml @@ -0,0 +1,57 @@ +name: 'application-deploy' +description: 'Application deployment' +inputs: + manifest-file: + description: 'Manifest file' + required: true + digest: + description: 'Repository image digest' + required: true + +runs: + using: "composite" + steps: + - name: Application deployment + run: | + set -eo pipefail + + MANIFEST_FILE="${{ inputs.manifest-file }}" + + if [ -z "$MANIFEST_FILE" ]; then + echo "No manifest file provided" + exit 1 + fi + + DIGEST="${{ inputs.digest }}" + + if [ -z "$DIGEST" ]; then + echo "No digest provided" + exit 1 + fi + + function export_json_to_env () { + service_name="$1" + + while IFS=$'\t\n' read -r LINE; do + export "${LINE}" + done < <( + <"${MANIFEST_FILE}" jq \ + --compact-output \ + --raw-output \ + --monochrome-output \ + --from-file \ + <(echo ".[\"${service_name}\"] | to_entries | map(\"\(.key)=\(.value)\") | .[]") + ) + } + + echo "Deploying ${MANIFEST_FILE}" + + mapfile -t services < <(jq -r 'keys[]' "$MANIFEST_FILE") + + for service_name in "${services[@]}"; do + export_json_to_env "$service_name" + aws eks update-kubeconfig --name "$eks_cluster_name" + kubectl -n "$eks_cluster_namespace" set image "$k8s_deployment_name" "${k8s_container_name}=${repository_url}@${DIGEST}" + kubectl -n "$eks_cluster_namespace" rollout restart "$k8s_deployment_name" + done + shell: bash \ No newline at end of file diff --git a/.github/actions/branch-specific-config/action.yml b/.github/actions/branch-specific-config/action.yml new file mode 100644 index 0000000..9ea23bf --- /dev/null +++ b/.github/actions/branch-specific-config/action.yml @@ -0,0 +1,75 @@ +name: 'branch-specific-config' +description: 'Determine branch-specific configuration' +outputs: + branch-name: + description: "Branch name" + value: ${{ steps.branch-specific-config.outputs.branch-name }} + image-rev: + description: "Docker image revision" + value: ${{ steps.branch-specific-config.outputs.image-rev }} + env: + description: "Environment" + value: ${{ steps.branch-specific-config.outputs.env }} + tf-workingdir: + description: "Terraform working directory" + value: ${{ steps.branch-specific-config.outputs.tf-workingdir }} + tf-lockfile: + description: "Terraform lockfile" + value: ${{ steps.branch-specific-config.outputs.tf-lockfile }} + docker-load: + description: "Docker load" + value: ${{ steps.branch-specific-config.outputs.docker-load }} + docker-push: + description: "Docker push" + value: ${{ steps.branch-specific-config.outputs.docker-push }} + manifests-dir: + description: "Manifests directory for the environment" + value: ${{ steps.branch-specific-config.outputs.manifests-dir }} + +runs: + using: "composite" + steps: + - name: Branch specific config + id: branch-specific-config + run: | + set -euxo pipefail + + PREFIX="refs/heads/" + BRANCH_NAME="${GITHUB_REF#"$PREFIX"}" + + ret=0 + git ls-remote --exit-code origin staging || ret=$? + if [ "${BRANCH_NAME}" = "main" ] || [ "${ret}" -eq 2 ]; then + echo "Running production build or running build in repo without a staging branch" + IMAGE_REV="latest" + ENV="prod" + else + echo "Running staging build" + IMAGE_REV="staging" + ENV="stg" + fi + + TF_WD="terraform/workspaces/${ENV}" + + TF_LF="${TF_WD}/.terraform.lock.hcl" + + DOCKER_LOAD="false" + DOCKER_PUSH="true" + + if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then + echo "Running a pull request - Load Docker image only, no push" + DOCKER_LOAD="true" + DOCKER_PUSH="false" + fi + + MANIFESTS_DIR="manifests/${ENV}" + + echo "::set-output name=branch-name::$(echo $BRANCH_NAME)" + echo "::set-output name=image-rev::$(echo $IMAGE_REV)" + echo "::set-output name=env::$(echo $ENV)" + echo "::set-output name=tf-workingdir::$(echo $TF_WD)" + echo "::set-output name=tf-lockfile::$(echo $TF_LF)" + echo "::set-output name=docker-load::$(echo $DOCKER_LOAD)" + echo "::set-output name=docker-push::$(echo $DOCKER_PUSH)" + echo "::set-output name=manifests-dir::$(echo $MANIFESTS_DIR)" + shell: bash \ No newline at end of file diff --git a/.github/actions/commit-changes/action.yml b/.github/actions/commit-changes/action.yml new file mode 100644 index 0000000..58205ea --- /dev/null +++ b/.github/actions/commit-changes/action.yml @@ -0,0 +1,30 @@ +name: 'commit-changes' +description: 'Commit changes to the GitHub repository' +inputs: + github-token: + description: 'GitHub token to commit with' + required: true + commit-message: + description: 'Commit message' + default: 'Committed automatically during workspace run' + required: false +runs: + using: "composite" + steps: + - uses: nick-invision/retry@v2 + with: + timeout_seconds: 15 + max_attempts: 3 + retry_on: error + command: | + echo "$GITHUB_CONTEXT" + git stash -u + git checkout "${GITHUB_REF:11}" + git pull + git stash apply ||: + git config --global user.name 'Devops Bot' + git config --global user.email 'devops-bot@flipsidecrypto.com' + git remote set-url origin https://x-access-token:${{ inputs.github-token }}@github.com/$GITHUB_REPOSITORY + git add -A + git commit -am "${{ inputs.commit-message }}" + git push ||: \ No newline at end of file diff --git a/.github/actions/docker-build-and-push/action.yml b/.github/actions/docker-build-and-push/action.yml new file mode 100644 index 0000000..c6a4c31 --- /dev/null +++ b/.github/actions/docker-build-and-push/action.yml @@ -0,0 +1,86 @@ +name: 'docker-build-and-push' +description: 'Docker build and push' +inputs: + base-dir: + description: 'Base directory' + required: false + default: './services' + service-name: + description: 'Service name' + required: true + repository-name: + description: 'Override the repository name' + required: false + default: '' +outputs: + registry: + description: "Registry" + value: ${{ steps.login-ecr.outputs.registry }} + service-name: + description: "Service name" + value: ${{ steps.docker-build-and-push-config.outputs.service-name }} + repository-name: + description: "Repository name" + value: ${{ steps.docker-build-and-push-config.outputs.repository-name }} + branch-name: + description: "Branch name" + value: ${{ steps.branch-specific-config.outputs.branch-name }} + image-rev: + description: "Docker image revision" + value: ${{ steps.branch-specific-config.outputs.image-rev }} + tf-workingdir: + description: "Terraform working directory" + value: ${{ steps.branch-specific-config.outputs.tf-workingdir }} + tf-lockfile: + description: "Terraform lockfile" + value: ${{ steps.branch-specific-config.outputs.tf-lockfile }} + docker-load: + description: "Docker load" + value: ${{ steps.branch-specific-config.outputs.docker-load }} + docker-push: + description: "Docker push" + value: ${{ steps.branch-specific-config.outputs.docker-push }} + manifests-dir: + description: "Manifests directory for the environment" + value: ${{ steps.branch-specific-config.outputs.manifests-dir }} + digest: + description: "Digest for the Docker repository image" + value: ${{ steps.docker-build-and-push.outputs.digest }} + repository-url: + description: "Repository URL for the Docker repository image" + value: ${{ steps.login-ecr.outputs.registry }}/${{ steps.docker-build-and-push-config.outputs.repository-name }} + +runs: + using: "composite" + steps: + - name: Docker Build and Push Config + id: docker-build-and-push-config + run: | + set -euxo pipefail + + SERVICE_NAME="${{ inputs.service-name }}" + SERVICE_NAME_SANITIZED="${SERVICE_NAME//_/-}" + + REPOSITORY_NAME="${{ inputs.repository-name }}" + if [ -z "$REPOSITORY_NAME" ]; then + REPOSITORY_NAME="${SERVICE_NAME_SANITIZED}" + fi + + echo "::set-output name=service-name::$(echo $SERVICE_NAME_SANITIZED)" + echo "::set-output name=repository-name::$(echo $REPOSITORY_NAME)" + shell: bash + - name: Login to Amazon ECR + id: login-ecr + uses: aws-actions/amazon-ecr-login@v1 + - name: Branch specific config + uses: ./.github/actions/branch-specific-config + id: branch-specific-config + - uses: docker/setup-buildx-action@v1 + - name: Build and push image + id: docker-build-and-push + uses: docker/build-push-action@v2 + with: + context: ${{ inputs.base-dir }}/${{ inputs.service-name }} + load: ${{ steps.branch-specific-config.outputs.docker-load }} + push: ${{ steps.branch-specific-config.outputs.docker-push }} + tags: ${{ steps.login-ecr.outputs.registry }}/${{ steps.docker-build-and-push-config.outputs.repository-name }}:${{ steps.branch-specific-config.outputs.image-rev }} \ No newline at end of file diff --git a/.github/actions/openvpn-connect/action.yml b/.github/actions/openvpn-connect/action.yml new file mode 100644 index 0000000..498470c --- /dev/null +++ b/.github/actions/openvpn-connect/action.yml @@ -0,0 +1,46 @@ +name: 'openvpn-connect' +description: 'Connect to an OpenVPN server using a config file' +inputs: + openvpn-profile: + description: 'OpenVPN profile (Must be base64-encoded)' + required: true + openvpn-version: + description: 'OpenVPN version to install' + required: false + default: '' + cache-key: + description: 'Cache key' + required: false + default: '' +outputs: + openvpn-config: + description: 'OpenVPN config file' + value: ${{ steps.setup-openvpn.outputs.openvpn-config }} + openvpn-log-dir: + description: 'OpenVPN log directory' + value: ${{ steps.run-openvpn.outputs.openvpn-log-dir }} + openvpn-version: + description: 'OpenVPN version' + value: ${{ steps.setup-openvpn.outputs.openvpn-version }} + cache-key: + description: 'Cache key' + value: ${{ steps.setup-openvpn.outputs.cache-key }} + +runs: + using: 'composite' + steps: + - name: Setup OpenVPN + uses: ./.github/actions/openvpn-setup + id: setup-openvpn + with: + openvpn-profile: ${{ inputs.openvpn-profile }} + openvpn-version: ${{ inputs.openvpn-version }} + cache-key: ${{ inputs.cache-key }} + - name: Connect to VPN Server + shell: bash + id: run-openvpn + run: | + OPENVPN_LOG_DIR="${RUNNER_TEMP}/logs" + mkdir -p "$OPENVPN_LOG_DIR" + sudo openvpn --config ${{ steps.setup-openvpn.outputs.openvpn-config }} --log "$(pwd)/vpn.log" --daemon + echo "::set-output name=openvpn-log-dir::${OPENVPN_LOG_DIR}" diff --git a/.github/actions/openvpn-kill/action.yml b/.github/actions/openvpn-kill/action.yml new file mode 100644 index 0000000..17e5c57 --- /dev/null +++ b/.github/actions/openvpn-kill/action.yml @@ -0,0 +1,35 @@ +name: 'openvpn-kill' +description: 'Kill a running connection to an OpenVPN server' +inputs: + openvpn-log-dir: + description: 'OpenVPN log directory' + required: false + default: "" +outputs: + openvpn-log-dir: + description: 'OpenVPN log directory' + value: ${{ steps.kill-openvpn.outputs.openvpn-log-dir }} + +runs: + using: 'composite' + steps: + - name: Kill VPN connection + id: kill-openvpn + shell: bash + run: | + OPENVPN_LOG_DIR="${{ inputs.openvpn-log-dir }}" + + if [[ ! -z "$OPENVPN_LOG_DIR" ]]; then + OPENVPN_LOG_DIR="${RUNNER_TEMP}/logs" + mkdir -p "$OPENVPN_LOG_DIR" + fi + + sudo chmod -Rv 777 "$OPENVPN_LOG_DIR" + echo "::set-output name=openvpn-log-dir::${OPENVPN_LOG_DIR}" + + sudo killall openvpn + - name: Upload VPN logs + uses: actions/upload-artifact@v2 + with: + name: VPN logs + path: ${{ steps.kill-openvpn.outputs.openvpn-log-dir }} diff --git a/.github/actions/openvpn-setup/action.yml b/.github/actions/openvpn-setup/action.yml new file mode 100644 index 0000000..7305dd9 --- /dev/null +++ b/.github/actions/openvpn-setup/action.yml @@ -0,0 +1,89 @@ +name: 'openvpn-setup' +description: 'Setup OpenVPN' +inputs: + openvpn-profile: + description: 'OpenVPN profile (Must be base64-encoded)' + required: true + openvpn-version: + description: 'OpenVPN version to install' + required: false + default: '' + cache-key: + description: 'Cache key' + required: false + default: '' +outputs: + openvpn-config: + description: 'OpenVPN config file' + value: ${{ steps.environment-setup.outputs.openvpn-config }} + openvpn-version: + description: 'OpenVPN version' + value: ${{ steps.openvpn-version.outputs.openvpn-version }} + cache-key: + description: 'Cache key' + value: ${{ steps.openvpn-version.outputs.cache-key }} + +runs: + using: 'composite' + steps: + - name: Get OpenVPN version + id: openvpn-version + shell: bash + run: | + set -euxo pipefail + + if [[ -z "$OPENVPN_VERSION" ]]; then + sudo apt-get update + + OPENVPN_VERSION="$(apt-cache policy openvpn | grep -oP '(?<=Candidate:\s)(.+)')" + fi + + echo "::set-output name=openvpn-version::${OPENVPN_VERSION}" + + if [[ -z "$CACHE_KEY" ]]; then + CACHE_KEY="${OPENVPN_VERSION}-$(date +%s)" + fi + + echo "::set-output name=cache-key::${CACHE_KEY}" + env: + CACHE_KEY: ${{ inputs.cache-key }} + OPENVPN_VERSION: ${{ inputs.openvpn-version }} + - name: Cache OpenVPN + uses: actions/cache@v2 + id: cache-openvpn + with: + path: "~/openvpn" + key: openvpn-${{ steps.openvpn-version.outputs.cache-key }} + restore-keys: | + openvpn-${{ steps.openvpn-version.outputs.cache-key }} + openvpn-${{ steps.openvpn-version.outputs.openvpn-version }} + openvpn- + - name: Install OpenVPN + shell: bash + run: | + set -euxo pipefail + + if [[ "$CACHE_HIT" == 'true' && -f "~/openvpn/openvpn_${OPENVPN_VERSION}_amd64.deb" ]]; then + sudo dpkg -i ~/openvpn/*.deb + else + sudo apt-get update + sudo apt-get clean + + sudo apt-get install --yes openvpn="$OPENVPN_VERSION" + + mkdir -p ~/openvpn && rm -rfv ~/openvpn/* + sudo cp -v /var/cache/apt/archives/*.deb ~/openvpn/ + sudo chown -Rv $(id -u):$(id -g) ~/openvpn/ + fi + env: + CACHE_HIT: ${{ steps.cache-openvpn.outputs.cache-hit }} + CACHE_KEY: ${{ steps.openvpn-version.outputs.cache-key }} + OPENVPN_VERSION: ${{ steps.openvpn-version.outputs.openvpn-version }} + - name: Environment setup + id: environment-setup + shell: bash + run: | + echo $OPENVPN_EXTERNAL_CI_PROFILE | base64 -d > ${{ runner.temp }}/config.ovpn + echo "::set-output name=openvpn-config::${{ runner.temp }}/config.ovpn" + env: + OPENVPN_EXTERNAL_CI_PROFILE: ${{ inputs.openvpn-profile }} \ No newline at end of file diff --git a/.github/actions/terraform-setup/action.yml b/.github/actions/terraform-setup/action.yml new file mode 100644 index 0000000..a22333f --- /dev/null +++ b/.github/actions/terraform-setup/action.yml @@ -0,0 +1,209 @@ +name: 'terraform-setup' +description: 'Sets up Terraform including custom providers' +inputs: + working-directory: + description: 'Terraform working directory in the repository' + required: true + github-token: + description: 'GitHub token for pulling custom providers from Flipside repos' + required: true + ssh-private-key: + description: 'Devops bot SSH key to use for pulling Flipside repos' + required: true + ssh-known-hosts: + description: 'Devops bot known hosts to use for pulling Flipside repos' + required: true + aws-access-key-id: + description: >- + AWS Access Key ID. This input is required if running in the GitHub hosted environment. + It is optional if running in a self-hosted environment that already has AWS credentials, + for example on an EC2 instance. + required: false + aws-secret-access-key: + description: >- + AWS Secret Access Key. This input is required if running in the GitHub hosted environment. + It is optional if running in a self-hosted environment that already has AWS credentials, + for example on an EC2 instance. + required: false + aws-session-token: + description: 'AWS Session Token' + required: false + aws-region: + description: 'AWS Region, e.g. us-east-2' + required: true + +runs: + using: "composite" + steps: + - name: Install SSH key + uses: shimataro/ssh-key-action@v2 + with: + key: ${{ inputs.ssh-private-key }} + known_hosts: ${{ inputs.ssh-known-hosts }} + if_key_exists: replace + - name: Configure AWS credentials + uses: aws-actions/configure-aws-credentials@v1 + with: + aws-access-key-id: ${{ inputs.aws-access-key-id }} + aws-secret-access-key: ${{ inputs.aws-secret-access-key }} + aws-session-token: ${{ inputs.aws-session-token }} + aws-region: ${{ inputs.aws-region }} + - uses: ./.github/actions/write-file + with: + path: ${{ inputs.working-directory }}/requirements.txt + contents: | + # + # These requirements were autogenerated by pipenv + # To regenerate from the project's Pipfile, run: + # + # pipenv lock --requirements + # + # In this module directory where the Pipfile for Terraform requirements is maintained. + + -i https://pypi.org/simple + boto3==1.21.24 + botocore==1.24.24; python_full_version >= '3.6.0' + cachetools==5.0.0; python_version ~= '3.7' + certifi==2021.10.8 + cffi==1.15.0 + charset-normalizer==2.0.12; python_version >= '3' + deepmerge==1.0.1 + deprecated==1.2.13; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' + fastcore==1.3.29; python_full_version >= '3.6.0' + ghapi==0.1.19 + gitdb==4.0.9; python_full_version >= '3.6.0' + gitpython==3.1.27 + google-auth==2.6.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' + idna==3.3; python_version >= '3' + jmespath==1.0.0; python_version >= '3.7' + kubernetes==23.3.0 + oauthlib==3.2.0; python_full_version >= '3.6.0' + packaging==21.3; python_full_version >= '3.6.0' + pem==21.2.0 + pip==22.0.4; python_version >= '3.7' + psycopg2-binary==2.9.3 + pyasn1-modules==0.2.8 + pyasn1==0.4.8 + pycparser==2.21 + pygithub==1.55 + pyjwt==2.3.0; python_full_version >= '3.6.0' + pynacl==1.5.0; python_full_version >= '3.6.0' + pyparsing==3.0.7; python_full_version >= '3.6.0' + python-dateutil==2.8.2; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' + pyyaml==6.0; python_full_version >= '3.6.0' + requests-oauthlib==1.3.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' + requests==2.27.1; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5' + retrying==1.3.3 + rsa==4.8; python_full_version >= '3.6.0' + ruamel.yaml.clib==0.2.6; python_version < '3.11' and platform_python_implementation == 'CPython' + ruamel.yaml==0.17.21 + s3transfer==0.5.2; python_full_version >= '3.6.0' + setuptools==60.10.0; python_version >= '3.7' + six==1.16.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3' + slack-sdk==3.15.2 + smmap==5.0.0; python_full_version >= '3.6.0' + urllib3==1.26.9; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4' + websocket-client==1.3.1; python_full_version >= '3.6.0' + wrapt==1.14.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' + - name: Setup Python + uses: actions/setup-python@v2 + with: + python-version: 3.9 + cache: 'pip' + cache-dependency-path: ${{ inputs.working-directory }}/requirements.txt + - name: Install Terraform Python requirements + shell: bash + run: | + pip install -r ${{ inputs.working-directory }}/requirements.txt + - name: Use Node.js 14.x + uses: actions/setup-node@v2 + with: + node-version: "14.x" + - uses: hashicorp/setup-terraform@v1 + with: + terraform_version: 1.1.5 + terraform_wrapper: false + - name: Setup Terraform plugin cache and local plugins root + shell: bash + run: | + echo 'plugin_cache_dir="$HOME/.terraform.d/plugin-cache"' > ~/.terraformrc + mkdir --parents ~/.terraform.d/plugin-cache + mkdir --parents ~/.terraform.d/plugins/terraform.flipsidecrypto.com/local + - name: Cache Terraform + uses: actions/cache@v2 + with: + path: | + ~/.terraform.d/plugin-cache + ~/.terraform.d/plugins + key: ${{ runner.os }}-terraform-v2-${{ hashFiles('${{ inputs.working-directory }}/.terraform.lock.hcl') }} + - name: Download latest custom Terraform providers + shell: bash + run: | + set -euxo pipefail + + for provider_bin in terraform-provider-googleworkspace; do + provider_name="${provider_bin##*-}" + + for platform in linux_amd64 darwin_amd64; do + provider_dir="${HOME}/.terraform.d/plugins/terraform.flipsidecrypto.com/local/${provider_name}/9.99.9/${platform}" + provider_bin_path="${provider_dir}/${provider_bin}" + + if [ -x "${provider_bin_path}" ]; then + echo "${provider_bin_path} restored from cache" + else + mkdir -p custom-providers + rm -rf custom-providers/* + gh release download --pattern "${provider_bin}_*_${platform}.zip" --repo "FlipsideCrypto/${provider_bin}" --dir ./custom-providers + mkdir -pv "$provider_dir" + + cd custom-providers + unzip -o "*.zip" + ls -lhat ./ + mv -fv "$provider_bin" "$provider_bin_path" + cd - + + rm -rf custom-providers + chmod +x "$provider_bin_path" + fi + done + + set +e + if ! terraform init --upgrade; then + rm -f .terraform.lock.hcl ||: + terraform providers lock -platform=darwin_amd64 -platform=linux_amd64 "terraform.flipsidecrypto.com/local/${provider_name}" + fi + set -e + done + working-directory: ${{ inputs.working-directory }} + env: + GITHUB_TOKEN: ${{ inputs.github-token }} + - name: Terraform Init + shell: bash + id: init + run: | + set -x + set +e + + run_again=0 + + if ! terraform init --upgrade; then + rm -f .terraform.lock.hcl ||: + terraform providers lock -platform=darwin_amd64 -platform=linux_amd64 + run_again=1 + fi + set -e + + if $run_again -gt 0; then + terraform init --upgrade + fi + working-directory: ${{ inputs.working-directory }} + - name: Terraform fmt + shell: bash + id: fmt + run: terraform fmt -recursive + working-directory: ${{ inputs.working-directory }} + - name: Terraform Validate + shell: bash + id: validate + run: terraform validate -no-color + working-directory: ${{ inputs.working-directory }} \ No newline at end of file diff --git a/.github/actions/write-file/action.yml b/.github/actions/write-file/action.yml new file mode 100644 index 0000000..29635c4 --- /dev/null +++ b/.github/actions/write-file/action.yml @@ -0,0 +1,27 @@ +name: 'write-file' +description: 'Write a file to the runner filesystem and set permissions on it' +inputs: + path: # id of input + description: "The path to the file to write" + required: true + contents: + description: "The contents of the file" + required: true + mode: + description: "The mode of the file in symbolic mode (e.g. a+x)" + default: "a+x" + required: false +runs: + using: "composite" + steps: + - name: Write file + shell: bash + id: write-file + run: | + set -euxo pipefail + + cat << EOF > ${{ inputs.path }} + ${{ inputs.contents }} + EOF + + chmod ${{ inputs.mode }} ${{ inputs.path }} \ No newline at end of file