├── .cfn-nag-ignore-lists.yml ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── config.yml │ ├── feature-request.md │ └── general-issues.md ├── pull_request_template.md ├── semantic.yaml └── workflows │ ├── bandit-check.yml │ ├── build.yml │ ├── cfn-nag.yml │ ├── lambda-integration-test.yml │ ├── pull-request-lint.yml │ └── upgrade.yml ├── .gitignore ├── .mergify.yml ├── .npmignore ├── .projen ├── deps.json ├── files.json └── tasks.json ├── .projenrc.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── LaunchStack.jpg ├── README.md ├── arch.png ├── cdk.json ├── jest.config.js ├── package.json ├── src ├── lambda.d │ ├── nexus3-purge │ │ └── index.py │ ├── nexuspreconfigure-integration-test │ │ ├── __init__.py │ │ ├── conftest.py │ │ ├── getBlobstores.groovy │ │ ├── requirements.txt │ │ ├── test_nexus.py │ │ └── wait-for-nexus.sh │ ├── nexuspreconfigure │ │ ├── __init__.py │ │ ├── createBlobstore.groovy │ │ ├── deleteDefaultBlobstore.groovy │ │ ├── index.py │ │ ├── nexus.py │ │ └── requirements.txt │ └── setup.cfg ├── lib │ └── sonatype-nexus3-stack.ts └── sonatype-nexus3.ts ├── test ├── context-provider-mock.ts └── sonatype-nexus3.test.ts ├── tsconfig.dev.json ├── tsconfig.json └── yarn.lock /.cfn-nag-ignore-lists.yml: -------------------------------------------------------------------------------- 1 | --- 2 | RulesToSuppress: 3 | - id: W33 4 | reason: allow assign public IPs in public subnet for solution 5 | - id: W58 6 | reason: use managed policy AWSLambdaBasicExecutionRole to grant logs permission in solution 7 | - id: W89 8 | reason: allow non VPC solution Lambda that are custom resource 9 | - id: W92 10 | reason: allow no ReservedConcurrentExecutions configuration of solution Lambda that are custom resource 11 | - id: W5 12 | reason: node group access internet is by design in solution 13 | - id: W40 14 | reason: node group access internet is by design in solution -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | // ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | { 3 | "env": { 4 | "jest": true, 5 | "node": true 6 | }, 7 | "root": true, 8 | "plugins": [ 9 | "@typescript-eslint", 10 | "import" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": 2018, 15 | "sourceType": "module", 16 | "project": "./tsconfig.dev.json" 17 | }, 18 | "extends": [ 19 | "plugin:import/typescript" 20 | ], 21 | "settings": { 22 | "import/parsers": { 23 | "@typescript-eslint/parser": [ 24 | ".ts", 25 | ".tsx" 26 | ] 27 | }, 28 | "import/resolver": { 29 | "node": {}, 30 | "typescript": { 31 | "project": "./tsconfig.dev.json", 32 | "alwaysTryTypes": true 33 | } 34 | } 35 | }, 36 | "ignorePatterns": [ 37 | "*.js", 38 | "!.projenrc.js", 39 | "*.d.ts", 40 | "node_modules/", 41 | "*.generated.ts", 42 | "coverage" 43 | ], 44 | "rules": { 45 | "indent": [ 46 | "off" 47 | ], 48 | "@typescript-eslint/indent": [ 49 | "error", 50 | 2 51 | ], 52 | "quotes": [ 53 | "error", 54 | "single", 55 | { 56 | "avoidEscape": true 57 | } 58 | ], 59 | "comma-dangle": [ 60 | "error", 61 | "always-multiline" 62 | ], 63 | "comma-spacing": [ 64 | "error", 65 | { 66 | "before": false, 67 | "after": true 68 | } 69 | ], 70 | "no-multi-spaces": [ 71 | "error", 72 | { 73 | "ignoreEOLComments": false 74 | } 75 | ], 76 | "array-bracket-spacing": [ 77 | "error", 78 | "never" 79 | ], 80 | "array-bracket-newline": [ 81 | "error", 82 | "consistent" 83 | ], 84 | "object-curly-spacing": [ 85 | "error", 86 | "always" 87 | ], 88 | "object-curly-newline": [ 89 | "error", 90 | { 91 | "multiline": true, 92 | "consistent": true 93 | } 94 | ], 95 | "object-property-newline": [ 96 | "error", 97 | { 98 | "allowAllPropertiesOnSameLine": true 99 | } 100 | ], 101 | "keyword-spacing": [ 102 | "error" 103 | ], 104 | "brace-style": [ 105 | "error", 106 | "1tbs", 107 | { 108 | "allowSingleLine": true 109 | } 110 | ], 111 | "space-before-blocks": [ 112 | "error" 113 | ], 114 | "curly": [ 115 | "error", 116 | "multi-line", 117 | "consistent" 118 | ], 119 | "@typescript-eslint/member-delimiter-style": [ 120 | "error" 121 | ], 122 | "semi": [ 123 | "error", 124 | "always" 125 | ], 126 | "max-len": [ 127 | "error", 128 | { 129 | "code": 150, 130 | "ignoreUrls": true, 131 | "ignoreStrings": true, 132 | "ignoreTemplateLiterals": true, 133 | "ignoreComments": true, 134 | "ignoreRegExpLiterals": true 135 | } 136 | ], 137 | "quote-props": [ 138 | "error", 139 | "consistent-as-needed" 140 | ], 141 | "@typescript-eslint/no-require-imports": [ 142 | "error" 143 | ], 144 | "import/no-extraneous-dependencies": [ 145 | "error", 146 | { 147 | "devDependencies": [ 148 | "**/test/**", 149 | "**/build-tools/**" 150 | ], 151 | "optionalDependencies": false, 152 | "peerDependencies": true 153 | } 154 | ], 155 | "import/no-unresolved": [ 156 | "error" 157 | ], 158 | "import/order": [ 159 | "warn", 160 | { 161 | "groups": [ 162 | "builtin", 163 | "external" 164 | ], 165 | "alphabetize": { 166 | "order": "asc", 167 | "caseInsensitive": true 168 | } 169 | } 170 | ], 171 | "no-duplicate-imports": [ 172 | "error" 173 | ], 174 | "no-shadow": [ 175 | "off" 176 | ], 177 | "@typescript-eslint/no-shadow": [ 178 | "error" 179 | ], 180 | "key-spacing": [ 181 | "error" 182 | ], 183 | "no-multiple-empty-lines": [ 184 | "error" 185 | ], 186 | "@typescript-eslint/no-floating-promises": [ 187 | "error" 188 | ], 189 | "no-return-await": [ 190 | "off" 191 | ], 192 | "@typescript-eslint/return-await": [ 193 | "error" 194 | ], 195 | "no-trailing-spaces": [ 196 | "error" 197 | ], 198 | "dot-notation": [ 199 | "error" 200 | ], 201 | "no-bitwise": [ 202 | "error" 203 | ], 204 | "@typescript-eslint/member-ordering": [ 205 | "error", 206 | { 207 | "default": [ 208 | "public-static-field", 209 | "public-static-method", 210 | "protected-static-field", 211 | "protected-static-method", 212 | "private-static-field", 213 | "private-static-method", 214 | "field", 215 | "constructor", 216 | "method" 217 | ] 218 | } 219 | ] 220 | }, 221 | "overrides": [ 222 | { 223 | "files": [ 224 | ".projenrc.js" 225 | ], 226 | "rules": { 227 | "@typescript-eslint/no-require-imports": "off", 228 | "import/no-extraneous-dependencies": "off" 229 | } 230 | } 231 | ] 232 | } 233 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | *.snap linguist-generated 4 | /.eslintrc.json linguist-generated 5 | /.gitattributes linguist-generated 6 | /.github/pull_request_template.md linguist-generated 7 | /.github/workflows/build.yml linguist-generated 8 | /.github/workflows/pull-request-lint.yml linguist-generated 9 | /.github/workflows/upgrade.yml linguist-generated 10 | /.gitignore linguist-generated 11 | /.mergify.yml linguist-generated 12 | /.npmignore linguist-generated 13 | /.projen/** linguist-generated 14 | /.projen/deps.json linguist-generated 15 | /.projen/files.json linguist-generated 16 | /.projen/tasks.json linguist-generated 17 | /cdk.json linguist-generated 18 | /package.json linguist-generated 19 | /tsconfig.dev.json linguist-generated 20 | /tsconfig.json linguist-generated 21 | /yarn.lock linguist-generated -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F41B Bug Report" 3 | about: Report a bug 4 | title: "" 5 | labels: bug, needs-triage 6 | --- 7 | 8 | 11 | 12 | 13 | ### Reproduction Steps 14 | 15 | 18 | 19 | 20 | 21 | ### Error Log 22 | 23 | 26 | 27 | 28 | 29 | ### Environment 30 | 31 | - **CDK CLI Version:** 32 | - **Framework Version:** 33 | - **Node.js Version:** 34 | - **OS :** 35 | 36 | ### Other 37 | 38 | 39 | 40 | 41 | 42 | --- 43 | 44 | This is :bug: Bug Report -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: Request a new feature 4 | title: "" 5 | labels: feature-request, needs-triage 6 | --- 7 | 8 | 9 | 10 | 11 | ### Use Case 12 | 13 | 14 | 15 | 16 | 17 | ### Proposed Solution 18 | 19 | 20 | 21 | 22 | 23 | ### Other 24 | 25 | 29 | 30 | 31 | 32 | * [ ] :wave: I may be able to implement this feature request 33 | 34 | --- 35 | 36 | This is a :rocket: Feature Request -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/general-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U00002753 General Issue" 3 | about: Create a new issue 4 | title: "" 5 | labels: needs-triage, guidance 6 | --- 7 | 8 | 9 | ## :question: General Issue 10 | 11 | 12 | 13 | ### The Question 14 | 20 | 21 | ### Environment 22 | 23 | - **CDK CLI Version:** 24 | - **Node.js Version:** 25 | - **OS:** 26 | 27 | 28 | ### Other information 29 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. -------------------------------------------------------------------------------- /.github/semantic.yaml: -------------------------------------------------------------------------------- 1 | # Configuration for Semantic Pull Requests 2 | titleAndCommits: true 3 | 4 | types: 5 | - feat 6 | - fix 7 | - docs 8 | - style 9 | - refactor 10 | - perf 11 | - test 12 | - build 13 | - ci 14 | - chore 15 | - revert 16 | - release 17 | 18 | -------------------------------------------------------------------------------- /.github/workflows/bandit-check.yml: -------------------------------------------------------------------------------- 1 | name: "Bnadit Check for Python code" 2 | 3 | on: 4 | pull_request: {} 5 | workflow_dispatch: {} 6 | workflow_run: 7 | workflows: 8 | - upgrade 9 | jobs: 10 | bandit: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - name: Run bandit 16 | uses: tj-actions/bandit@v4.1 17 | with: 18 | version: "1.7.0" 19 | targets: | 20 | src/lambda.d/ 21 | options: "-r -s B301 -ll" 22 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: build 4 | on: 5 | pull_request: {} 6 | workflow_dispatch: {} 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | contents: write 12 | outputs: 13 | self_mutation_happened: ${{ steps.self_mutation.outputs.self_mutation_happened }} 14 | env: 15 | CI: "true" 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | with: 20 | ref: ${{ github.event.pull_request.head.ref }} 21 | repository: ${{ github.event.pull_request.head.repo.full_name }} 22 | - name: Install dependencies 23 | run: yarn install --check-files 24 | - name: build 25 | run: npx projen build 26 | - name: Find mutations 27 | id: self_mutation 28 | run: |- 29 | git add . 30 | git diff --staged --patch --exit-code > .repo.patch || echo "self_mutation_happened=true" >> $GITHUB_OUTPUT 31 | - name: Upload patch 32 | if: steps.self_mutation.outputs.self_mutation_happened 33 | uses: actions/upload-artifact@v3 34 | with: 35 | name: .repo.patch 36 | path: .repo.patch 37 | - name: Fail build on mutation 38 | if: steps.self_mutation.outputs.self_mutation_happened 39 | run: |- 40 | echo "::error::Files were changed during build (see build log). If this was triggered from a fork, you will need to update your branch." 41 | cat .repo.patch 42 | exit 1 43 | self-mutation: 44 | needs: build 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: write 48 | if: always() && needs.build.outputs.self_mutation_happened && !(github.event.pull_request.head.repo.full_name != github.repository) 49 | steps: 50 | - name: Checkout 51 | uses: actions/checkout@v3 52 | with: 53 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 54 | ref: ${{ github.event.pull_request.head.ref }} 55 | repository: ${{ github.event.pull_request.head.repo.full_name }} 56 | - name: Download patch 57 | uses: actions/download-artifact@v3 58 | with: 59 | name: .repo.patch 60 | path: ${{ runner.temp }} 61 | - name: Apply patch 62 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 63 | - name: Set git identity 64 | run: |- 65 | git config user.name "github-actions" 66 | git config user.email "github-actions@github.com" 67 | - name: Push changes 68 | run: |2- 69 | git add . 70 | git commit -s -m "chore: self mutation" 71 | git push origin HEAD:${{ github.event.pull_request.head.ref }} 72 | -------------------------------------------------------------------------------- /.github/workflows/cfn-nag.yml: -------------------------------------------------------------------------------- 1 | name: Cfn-nag check 2 | 'on': 3 | pull_request: {} 4 | workflow_dispatch: {} 5 | workflow_run: 6 | workflows: 7 | - upgrade 8 | jobs: 9 | cfn-nag: 10 | runs-on: ubuntu-latest 11 | env: 12 | CI: 'true' 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | with: 17 | ref: '${{ github.event.pull_request.head.ref }}' 18 | repository: '${{ github.event.pull_request.head.repo.full_name }}' 19 | - name: Install dependencies 20 | run: yarn install --check-files --frozen-lockfile 21 | - name: synth 22 | run: npx cdk synth 23 | - uses: stelligent/cfn_nag@master 24 | with: 25 | input_path: cdk.out/SonatypeNexus3OnEKS.template.json 26 | extra_args: --fail-on-warnings -b .cfn-nag-ignore-lists.yml --print-suppression 27 | -------------------------------------------------------------------------------- /.github/workflows/lambda-integration-test.yml: -------------------------------------------------------------------------------- 1 | name: integration test of nexus preconfigure lambda 2 | 'on': 3 | push: 4 | branches: 5 | - master 6 | pull_request: {} 7 | workflow_dispatch: {} 8 | workflow_run: 9 | workflows: 10 | - upgrade 11 | jobs: 12 | lambda-integration-test: 13 | name: Lambda Integaration Test 14 | runs-on: '${{ matrix.os }}' 15 | strategy: 16 | matrix: 17 | os: 18 | - ubuntu-18.04 19 | - ubuntu-20.04 20 | python-version: 21 | - 3.8 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: 'Use python ${{ matrix.python-version }}' 25 | uses: actions/setup-python@v2 26 | with: 27 | python-version: '${{ matrix.python-version }}' 28 | - name: Replace Nexus properties 29 | run: > 30 | docker exec nexus sh -c "echo 'nexus.scripts.allowCreation=true' >> 31 | /nexus-data/etc/nexus.properties" 32 | - name: Restart Nexus 33 | uses: 'docker://docker' 34 | with: 35 | args: docker restart nexus 36 | - name: Install dependencies 37 | run: > 38 | python -m pip install --upgrade pip 39 | 40 | pip install -r src/lambda.d/nexuspreconfigure/requirements.txt 41 | 42 | pip install -r 43 | src/lambda.d/nexuspreconfigure-integration-test/requirements.txt 44 | - name: Fetch Nexus3 Password 45 | id: nexus3-pass 46 | run: | 47 | bash src/lambda.d/nexuspreconfigure-integration-test/wait-for-nexus.sh 48 | NEXUS_PASS=`docker exec nexus cat /nexus-data/admin.password` 49 | echo "::set-output name=NEXUS_PASS::$NEXUS_PASS" 50 | - name: Integration test 51 | env: 52 | NEXUS_PASS: '${{ steps.nexus3-pass.outputs.NEXUS_PASS }}' 53 | run: | 54 | cd src/lambda.d/ 55 | pytest -m integration 56 | services: 57 | nexus: 58 | image: 'quay.io/travelaudience/docker-nexus:3.37.3-02' 59 | ports: 60 | - '8081:8081' 61 | options: >- 62 | --name nexus --rm --health-cmd "wget --server-response --spider 63 | --quiet 'http://localhost:8081' 2>&1" --health-interval 10s 64 | --health-timeout 3s --health-retries 30 65 | -------------------------------------------------------------------------------- /.github/workflows/pull-request-lint.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: pull-request-lint 4 | on: 5 | pull_request_target: 6 | types: 7 | - labeled 8 | - opened 9 | - synchronize 10 | - reopened 11 | - ready_for_review 12 | - edited 13 | jobs: 14 | validate: 15 | name: Validate PR title 16 | runs-on: ubuntu-latest 17 | permissions: 18 | pull-requests: write 19 | steps: 20 | - uses: amannn/action-semantic-pull-request@v5.0.2 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | with: 24 | types: |- 25 | feat 26 | fix 27 | chore 28 | requireScope: false 29 | -------------------------------------------------------------------------------- /.github/workflows/upgrade.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | name: upgrade 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 0 * * * 8 | jobs: 9 | upgrade: 10 | name: Upgrade 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | outputs: 15 | patch_created: ${{ steps.create_patch.outputs.patch_created }} 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | - name: Install dependencies 20 | run: yarn install --check-files --frozen-lockfile 21 | - name: Upgrade dependencies 22 | run: npx projen upgrade 23 | - name: Find mutations 24 | id: create_patch 25 | run: |- 26 | git add . 27 | git diff --staged --patch --exit-code > .repo.patch || echo "patch_created=true" >> $GITHUB_OUTPUT 28 | - name: Upload patch 29 | if: steps.create_patch.outputs.patch_created 30 | uses: actions/upload-artifact@v3 31 | with: 32 | name: .repo.patch 33 | path: .repo.patch 34 | pr: 35 | name: Create Pull Request 36 | needs: upgrade 37 | runs-on: ubuntu-latest 38 | permissions: 39 | contents: read 40 | if: ${{ needs.upgrade.outputs.patch_created }} 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v3 44 | with: {} 45 | - name: Download patch 46 | uses: actions/download-artifact@v3 47 | with: 48 | name: .repo.patch 49 | path: ${{ runner.temp }} 50 | - name: Apply patch 51 | run: '[ -s ${{ runner.temp }}/.repo.patch ] && git apply ${{ runner.temp }}/.repo.patch || echo "Empty patch. Skipping."' 52 | - name: Set git identity 53 | run: |- 54 | git config user.name "github-actions" 55 | git config user.email "github-actions@github.com" 56 | - name: Create Pull Request 57 | id: create-pr 58 | uses: peter-evans/create-pull-request@v4 59 | with: 60 | token: ${{ secrets.PROJEN_GITHUB_TOKEN }} 61 | commit-message: |- 62 | chore(deps): upgrade dependencies 63 | 64 | Upgrades project dependencies. See details in [workflow run]. 65 | 66 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 67 | 68 | ------ 69 | 70 | *Automatically created by projen via the "upgrade" workflow* 71 | branch: github-actions/upgrade 72 | title: "chore(deps): upgrade dependencies" 73 | labels: auto-approve,auto-merge 74 | body: |- 75 | Upgrades project dependencies. See details in [workflow run]. 76 | 77 | [Workflow Run]: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} 78 | 79 | ------ 80 | 81 | *Automatically created by projen via the "upgrade" workflow* 82 | author: github-actions 83 | committer: github-actions 84 | signoff: true 85 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | !/.gitattributes 3 | !/.projen/tasks.json 4 | !/.projen/deps.json 5 | !/.projen/files.json 6 | !/.github/workflows/pull-request-lint.yml 7 | !/package.json 8 | !/.npmignore 9 | logs 10 | *.log 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | lerna-debug.log* 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | lib-cov 21 | coverage 22 | *.lcov 23 | .nyc_output 24 | build/Release 25 | node_modules/ 26 | jspm_packages/ 27 | *.tsbuildinfo 28 | .eslintcache 29 | *.tgz 30 | .yarn-integrity 31 | .cache 32 | .idea/ 33 | .vscode/ 34 | cdk.context.json 35 | .DS_Store 36 | !/.projenrc.js 37 | /test-reports/ 38 | junit.xml 39 | /coverage/ 40 | !/.github/workflows/build.yml 41 | !/.mergify.yml 42 | !/.github/workflows/upgrade.yml 43 | !/.github/pull_request_template.md 44 | !/test/ 45 | !/tsconfig.json 46 | !/tsconfig.dev.json 47 | !/src/ 48 | /lib 49 | /dist/ 50 | !/.eslintrc.json 51 | /assets/ 52 | !/cdk.json 53 | /cdk.out/ 54 | .cdk.staging/ 55 | .parcel-cache/ 56 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | 3 | queue_rules: 4 | - name: default 5 | conditions: 6 | - "#approved-reviews-by>=1" 7 | - -label~=(do-not-merge) 8 | - status-success=build 9 | pull_request_rules: 10 | - name: Automatic merge on approval and successful build 11 | actions: 12 | delete_head_branch: {} 13 | queue: 14 | method: squash 15 | name: default 16 | commit_message_template: |- 17 | {{ title }} (#{{ number }}) 18 | 19 | {{ body }} 20 | conditions: 21 | - "#approved-reviews-by>=1" 22 | - -label~=(do-not-merge) 23 | - status-success=build 24 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # ~~ Generated by projen. To modify, edit .projenrc.js and run "npx projen". 2 | /.projen/ 3 | /test-reports/ 4 | junit.xml 5 | /coverage/ 6 | permissions-backup.acl 7 | /.mergify.yml 8 | /test/ 9 | /tsconfig.dev.json 10 | /src/ 11 | !/lib/ 12 | !/lib/**/*.js 13 | !/lib/**/*.d.ts 14 | dist 15 | /tsconfig.json 16 | /.github/ 17 | /.vscode/ 18 | /.idea/ 19 | /.projenrc.js 20 | tsconfig.tsbuildinfo 21 | /.eslintrc.json 22 | !/assets/ 23 | cdk.out/ 24 | .cdk.staging/ 25 | -------------------------------------------------------------------------------- /.projen/deps.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "@types/jest", 5 | "type": "build" 6 | }, 7 | { 8 | "name": "@types/node", 9 | "version": "^14", 10 | "type": "build" 11 | }, 12 | { 13 | "name": "@typescript-eslint/eslint-plugin", 14 | "version": "^5", 15 | "type": "build" 16 | }, 17 | { 18 | "name": "@typescript-eslint/parser", 19 | "version": "^5", 20 | "type": "build" 21 | }, 22 | { 23 | "name": "aws-cdk", 24 | "version": "2.37.1", 25 | "type": "build" 26 | }, 27 | { 28 | "name": "esbuild", 29 | "type": "build" 30 | }, 31 | { 32 | "name": "eslint-import-resolver-node", 33 | "type": "build" 34 | }, 35 | { 36 | "name": "eslint-import-resolver-typescript", 37 | "type": "build" 38 | }, 39 | { 40 | "name": "eslint-plugin-import", 41 | "type": "build" 42 | }, 43 | { 44 | "name": "eslint", 45 | "version": "^8", 46 | "type": "build" 47 | }, 48 | { 49 | "name": "jest", 50 | "type": "build" 51 | }, 52 | { 53 | "name": "jest-junit", 54 | "version": "^13", 55 | "type": "build" 56 | }, 57 | { 58 | "name": "json-schema", 59 | "type": "build" 60 | }, 61 | { 62 | "name": "lodash", 63 | "version": ">=4.17.21", 64 | "type": "build" 65 | }, 66 | { 67 | "name": "npm-check-updates", 68 | "version": "^16", 69 | "type": "build" 70 | }, 71 | { 72 | "name": "projen", 73 | "type": "build" 74 | }, 75 | { 76 | "name": "ts-jest", 77 | "type": "build" 78 | }, 79 | { 80 | "name": "ts-node", 81 | "type": "build" 82 | }, 83 | { 84 | "name": "typescript", 85 | "version": "~4.6.0", 86 | "type": "build" 87 | }, 88 | { 89 | "name": "@aws-cdk/aws-lambda-python-alpha", 90 | "type": "runtime" 91 | }, 92 | { 93 | "name": "aws-cdk-lib", 94 | "version": "2.37.1", 95 | "type": "runtime" 96 | }, 97 | { 98 | "name": "constructs", 99 | "version": "^10.0.5", 100 | "type": "runtime" 101 | }, 102 | { 103 | "name": "js-yaml", 104 | "version": "^3.14.1", 105 | "type": "runtime" 106 | }, 107 | { 108 | "name": "sync-request", 109 | "version": "^6.1.0", 110 | "type": "runtime" 111 | } 112 | ], 113 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 114 | } 115 | -------------------------------------------------------------------------------- /.projen/files.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | ".eslintrc.json", 4 | ".gitattributes", 5 | ".github/pull_request_template.md", 6 | ".github/workflows/build.yml", 7 | ".github/workflows/pull-request-lint.yml", 8 | ".github/workflows/upgrade.yml", 9 | ".gitignore", 10 | ".mergify.yml", 11 | ".npmignore", 12 | ".projen/deps.json", 13 | ".projen/files.json", 14 | ".projen/tasks.json", 15 | "cdk.json", 16 | "tsconfig.dev.json", 17 | "tsconfig.json" 18 | ], 19 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 20 | } 21 | -------------------------------------------------------------------------------- /.projen/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasks": { 3 | "build": { 4 | "name": "build", 5 | "description": "Full release build", 6 | "steps": [ 7 | { 8 | "spawn": "default" 9 | }, 10 | { 11 | "spawn": "pre-compile" 12 | }, 13 | { 14 | "spawn": "compile" 15 | }, 16 | { 17 | "spawn": "post-compile" 18 | }, 19 | { 20 | "spawn": "test" 21 | }, 22 | { 23 | "spawn": "package" 24 | } 25 | ] 26 | }, 27 | "bundle": { 28 | "name": "bundle", 29 | "description": "Prepare assets" 30 | }, 31 | "clobber": { 32 | "name": "clobber", 33 | "description": "hard resets to HEAD of origin and cleans the local repo", 34 | "env": { 35 | "BRANCH": "$(git branch --show-current)" 36 | }, 37 | "steps": [ 38 | { 39 | "exec": "git checkout -b scratch", 40 | "name": "save current HEAD in \"scratch\" branch" 41 | }, 42 | { 43 | "exec": "git checkout $BRANCH" 44 | }, 45 | { 46 | "exec": "git fetch origin", 47 | "name": "fetch latest changes from origin" 48 | }, 49 | { 50 | "exec": "git reset --hard origin/$BRANCH", 51 | "name": "hard reset to origin commit" 52 | }, 53 | { 54 | "exec": "git clean -fdx", 55 | "name": "clean all untracked files" 56 | }, 57 | { 58 | "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" 59 | } 60 | ], 61 | "condition": "git diff --exit-code > /dev/null" 62 | }, 63 | "compile": { 64 | "name": "compile", 65 | "description": "Only compile" 66 | }, 67 | "default": { 68 | "name": "default", 69 | "description": "Synthesize project files", 70 | "steps": [ 71 | { 72 | "exec": "node .projenrc.js" 73 | } 74 | ] 75 | }, 76 | "deploy": { 77 | "name": "deploy", 78 | "description": "Deploys your CDK app to the AWS cloud", 79 | "steps": [ 80 | { 81 | "exec": "cdk deploy", 82 | "receiveArgs": true 83 | } 84 | ] 85 | }, 86 | "destroy": { 87 | "name": "destroy", 88 | "description": "Destroys your cdk app in the AWS cloud", 89 | "steps": [ 90 | { 91 | "exec": "cdk destroy", 92 | "receiveArgs": true 93 | } 94 | ] 95 | }, 96 | "diff": { 97 | "name": "diff", 98 | "description": "Diffs the currently deployed app against your code", 99 | "steps": [ 100 | { 101 | "exec": "cdk diff" 102 | } 103 | ] 104 | }, 105 | "eject": { 106 | "name": "eject", 107 | "description": "Remove projen from the project", 108 | "env": { 109 | "PROJEN_EJECTING": "true" 110 | }, 111 | "steps": [ 112 | { 113 | "spawn": "default" 114 | } 115 | ] 116 | }, 117 | "eslint": { 118 | "name": "eslint", 119 | "description": "Runs eslint against the codebase", 120 | "steps": [ 121 | { 122 | "exec": "eslint --ext .ts,.tsx --fix --no-error-on-unmatched-pattern src test build-tools .projenrc.js" 123 | } 124 | ] 125 | }, 126 | "package": { 127 | "name": "package", 128 | "description": "Creates the distribution package" 129 | }, 130 | "post-compile": { 131 | "name": "post-compile", 132 | "description": "Runs after successful compilation", 133 | "steps": [ 134 | { 135 | "spawn": "synth:silent" 136 | } 137 | ] 138 | }, 139 | "post-upgrade": { 140 | "name": "post-upgrade", 141 | "description": "Runs after upgrading dependencies" 142 | }, 143 | "pre-compile": { 144 | "name": "pre-compile", 145 | "description": "Prepare the project for compilation" 146 | }, 147 | "synth": { 148 | "name": "synth", 149 | "description": "Synthesizes your cdk app into cdk.out", 150 | "steps": [ 151 | { 152 | "exec": "cdk synth -c createNewVpc=true" 153 | } 154 | ] 155 | }, 156 | "synth:silent": { 157 | "name": "synth:silent", 158 | "description": "Synthesizes your cdk app into cdk.out and suppresses the template in stdout (part of \"yarn build\")", 159 | "steps": [ 160 | { 161 | "exec": "cdk synth -q" 162 | } 163 | ] 164 | }, 165 | "test": { 166 | "name": "test", 167 | "description": "Run tests", 168 | "steps": [ 169 | { 170 | "exec": "jest --passWithNoTests --updateSnapshot", 171 | "receiveArgs": true 172 | }, 173 | { 174 | "spawn": "eslint" 175 | } 176 | ] 177 | }, 178 | "test:watch": { 179 | "name": "test:watch", 180 | "description": "Run jest in watch mode", 181 | "steps": [ 182 | { 183 | "exec": "jest --watch" 184 | } 185 | ] 186 | }, 187 | "upgrade": { 188 | "name": "upgrade", 189 | "description": "upgrade dependencies", 190 | "env": { 191 | "CI": "0" 192 | }, 193 | "steps": [ 194 | { 195 | "exec": "yarn upgrade npm-check-updates" 196 | }, 197 | { 198 | "exec": "npm-check-updates --dep dev --upgrade --target=minor --reject='aws-cdk,lodash,typescript,aws-cdk-lib'" 199 | }, 200 | { 201 | "exec": "npm-check-updates --dep optional --upgrade --target=minor --reject='aws-cdk,lodash,typescript,aws-cdk-lib'" 202 | }, 203 | { 204 | "exec": "npm-check-updates --dep peer --upgrade --target=minor --reject='aws-cdk,lodash,typescript,aws-cdk-lib'" 205 | }, 206 | { 207 | "exec": "npm-check-updates --dep prod --upgrade --target=minor --reject='aws-cdk,lodash,typescript,aws-cdk-lib'" 208 | }, 209 | { 210 | "exec": "npm-check-updates --dep bundle --upgrade --target=minor --reject='aws-cdk,lodash,typescript,aws-cdk-lib'" 211 | }, 212 | { 213 | "exec": "yarn install --check-files" 214 | }, 215 | { 216 | "exec": "yarn upgrade" 217 | }, 218 | { 219 | "exec": "npx projen" 220 | }, 221 | { 222 | "spawn": "post-upgrade" 223 | } 224 | ] 225 | }, 226 | "watch": { 227 | "name": "watch", 228 | "description": "Watches changes in your source code and rebuilds and deploys to the current account", 229 | "steps": [ 230 | { 231 | "exec": "cdk deploy --hotswap" 232 | }, 233 | { 234 | "exec": "cdk watch" 235 | } 236 | ] 237 | } 238 | }, 239 | "env": { 240 | "PATH": "$(npx -c \"node -e \\\"console.log(process.env.PATH)\\\"\")" 241 | }, 242 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 243 | } 244 | -------------------------------------------------------------------------------- /.projenrc.js: -------------------------------------------------------------------------------- 1 | const { awscdk } = require('projen'); 2 | const project = new awscdk.AwsCdkTypeScriptApp({ 3 | cdkVersion: '2.37.1', 4 | defaultReleaseBranch: 'master', 5 | name: 'sonatype-nexus3', 6 | appEntrypoint: 'sonatype-nexus3.ts', 7 | cdkVersionPinning: true, 8 | deps: [ 9 | 'js-yaml@^3.14.1', 10 | 'sync-request@^6.1.0', 11 | '@aws-cdk/aws-lambda-python-alpha', 12 | ], /* Runtime dependencies of this module. */ 13 | // description: undefined, /* The description is just a string that helps people understand the purpose of the package. */ 14 | devDeps: [ 15 | 'lodash@>=4.17.21', 16 | ], /* Build dependencies for this module. */ 17 | typescriptVersion: '~4.6.0', /* TypeScript version to use. */ 18 | // packageName: undefined, /* The "name" in package.json. */ 19 | // projectType: ProjectType.UNKNOWN, /* Which type of project this is (library/app). */ 20 | // releaseWorkflow: undefined, /* Define a GitHub workflow for releasing from "main" when new versions are bumped. */ 21 | pullRequestTemplate: true /* Include a GitHub pull request template. */, 22 | pullRequestTemplateContents: [ 23 | '*Issue #, if available:*', 24 | '', 25 | '*Description of changes:*', 26 | '', 27 | '', 28 | 'By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.', 29 | ], 30 | license: 'MIT-0' /* License's SPDX identifier. */, 31 | licensed: false /* Indicates if a license should be added. */, 32 | gitignore: [ 33 | '.idea/', 34 | '.vscode/', 35 | 'cdk.context.json', 36 | '.DS_Store', 37 | ], 38 | keywords: [ 39 | 'aws', 40 | 'sonatype', 41 | 'nexus3', 42 | 'aws-cdk', 43 | 'aws-eks', 44 | 'eks', 45 | ], 46 | depsUpgradeOptions: { 47 | ignoreProjen: false, 48 | workflowOptions: { 49 | labels: ['auto-approve', 'auto-merge'], 50 | secret: 'PROJEN_GITHUB_TOKEN', 51 | }, 52 | }, 53 | }); 54 | // tricky to override the default synth task 55 | project.tasks._tasks.synth._steps[0] = { 56 | exec: 'cdk synth -c createNewVpc=true', 57 | }; 58 | // project.package.addField('resolutions', 59 | // Object.assign({}, project.package.manifest.resolutions ? project.package.manifest.resolutions : {}, { 60 | // 'pac-resolver': '^5.0.0', 61 | // 'set-value': '^4.0.1', 62 | // 'ansi-regex': '^5.0.1', 63 | // }) 64 | // ); 65 | project.addFields({ 66 | version: '1.3.0-mainline', 67 | }); 68 | project.synth(); -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /LaunchStack.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nexus-oss-on-aws/2942e924c2fe6d273ebfcb85f274cad9f2167759/LaunchStack.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sonatype Nexus Repository OSS on Amazon EKS 2 | 3 | Deploy Sonatype Nexus Repository OSS via Helm on EKS. 4 | 5 | - Use EFS via EFS CSI driver, PV and PVC as Nexus3 data storage 6 | - Create a dedicated S3 bucket as Nexus3 blobstore 7 | - Use external DNS to create record in Route53 for ingress domain name 8 | - Use ACM to get certificate of domain name 9 | 10 | ## Architecture diagram 11 | ![architecture diagram](arch.png) 12 | 13 | ## Usage 14 | 15 | ### Prerequisites 16 | - An AWS account 17 | - Nodejs LTS installed, such as 12.x or 14.x 18 | - Install Docker Engine 19 | - A public hosted zone in Route53(optional) 20 | - Has default VPC with public and private subnets cross two available zones at least, NAT gateway also is required 21 | - Install dependencies of app 22 | ``` 23 | yarn install --check-files --frozen-lockfile 24 | npx projen 25 | ``` 26 | 27 | ### Deployment 28 | #### Deploy with custom domain 29 | ``` 30 | npx cdk deploy --parameters NexusAdminInitPassword= --parameters DomainName= 31 | ``` 32 | 33 | #### Deploy with Route53 managed domain name 34 | ``` 35 | npx cdk deploy --parameters NexusAdminInitPassword= --parameters DomainName= -c r53Domain= 36 | ``` 37 | or 38 | ``` 39 | npx cdk deploy --parameters NexusAdminInitPassword= --parameters DomainName= --parameters R53HostedZoneId= -c enableR53HostedZone=true 40 | ``` 41 | 42 | #### Deploy to an existing VPC 43 | This solution will create new VPC across two AZs with public, private subnets and NAT gateways by default. 44 | 45 | You can deploy the solution to the existing VPC by below options, 46 | ``` 47 | npx cdk deploy -c vpcId= 48 | 49 | # or deploy to the default vpc 50 | npx cdk deploy -c vpcId=default 51 | ``` 52 | 53 | **NOTE**: the existing VPC must have public and private subnets across two AZs and route the internet traffic of private subnets to NAT gateways. 54 | 55 | #### Deploy with internal load balancer 56 | ``` 57 | npx cdk deploy -c internalALB=true 58 | ``` 59 | 60 | #### Customize the version of Kubernetes 61 | The solution will create [Kubernetes 1.20](https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.20) by default. You can specify other Kubernetes versions like below, 62 | ``` 63 | npx cdk deploy --parameters KubernetesVersion=1.19 64 | ``` 65 | 66 | **NOTE**: `1.20`, `1.19` and `1.18` are allowed versions. You can NOT enable [auto configuration feat](#auto-configuration) when creating an EKS cluster with version **1.19**. See [this issue](https://github.com/aws/aws-cdk/issues/14933) for detail. 67 | 68 | #### Deploy to China regions 69 | Due to AWS load balancer has different policy requirement for partitions, you need speicfy the target region info via context `region` to pick the corresponding IAM policies. 70 | ``` 71 | npx cdk deploy -c region=cn-north-1 72 | ``` 73 | 74 | #### Deploy to existing EKS cluster 75 | The solution could deploy the Nexus Repository OSS to the existing EKS cluster. There are some prerequisites that your EKS cluster must meet, 76 | 77 | - the version of EKS cluster is v1.17+, 78 | - the EKS cluster has EC2 based node group which is required by EFS CSI driver, 79 | - the ARN of an IAM role mapped to the `system:masters` RBAC role. If the cluster you are using was created using the AWS CDK, the CloudFormation stack has an output that includes an IAM role that can be used. Otherwise, you can create an IAM role and map it to `system:masters` manually. The trust policy of this role should include the the `arn:aws::iam::${accountId}:root` principal in order to allow the execution role of the kubectl resource to assume it. Then you can follow the [eksctl guide](https://eksctl.io/usage/iam-identity-mappings/) to map the IAM role to Kubernetes RBAC, 80 | - the OpenId connect provider ARN of your EKS. You can find the ARN from IAM's console. If your cluster does not have an OpenId connect provider, you can follow the [eksctl guide](https://eksctl.io/usage/iamserviceaccounts/) to create one, 81 | - the ARN of the IAM role associated with the nodegroup in your cluster. You can find the ARN of node group from EKS console. 82 | 83 | Below is an example to deploy Nexus Repository OSS to an existing EKS cluster with public domain configured, 84 | ```bash 85 | npx cdk deploy -c vpcId=vpc-12345 -c importedEKS=true -c eksClusterName=the-cluster-name -c eksKubectlRoleArn=arn:aws:iam::123456789012:role/eks-kubectl-role -c eksOpenIdConnectProviderArn=arn:aws:iam::123456789012:oidc-provider/oidc.eks.ap-east-1.amazonaws.com/id/12345678 -c nodeGroupRoleArn=arn:aws:iam::123456789012:role/eksctl-cluster-nodegroup-ng-NodeInstanceRole-123456 --parameters NexusAdminInitPassword= -c enableAutoConfigured=true --parameters DomainName= --parameters R53HostedZoneId= -c enableR53HostedZone=true 86 | ``` 87 | 88 | ### Init admin password 89 | You must specify the default init admin password when deploying this solution. The password must satisfy below requirements, 90 | - at least 8 characters 91 | - must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number 92 | - can contain special characters 93 | 94 | ### Auto configuration 95 | Nexus3 supports using [script][nexus3-script] to configure the Nexus3 service, for example, BlobStores, Repositories and so on. The script feature is disabled by default since Nexus3 3.21.2. You can opt-in auto configuration feature of this solution like below that will enable script feature of Nexus. 96 | ``` 97 | npx cdk deploy -c enableAutoConfigured=true 98 | ``` 99 | It would automatically configure the fresh provisioning Nexus3 with below changes, 100 | 101 | - Delete all built-in repositories 102 | - Delete default `file` based blobstore 103 | - Create a new blobstore named `s3-blobstore` using the dedicated S3 bucket created by this solution with never expiration policy for artifacts 104 | 105 | ### How to clean 106 | Run below command to clean the deployment or delete the `SonatypeNexus3OnEKS` stack via CloudFormation console. 107 | ``` 108 | npx cdk destroy 109 | ``` 110 | **NOTE**: you still need manually delete the EFS file system and S3 bucket created by this solution. Those storage might contain your data, be caution before deleting them. 111 | 112 | ## Quick deployment 113 | It's [an official solution][nexus-oss-on-aws-solution] of AWS China regions. You can quickly deploy this solution to below regions via CloudFormation, 114 | 115 | ### Deploy Nexus Repository OSS as a public service 116 | Region name | Region code | Launch 117 | --- | --- | --- 118 | Global regions(switch to the region you want to deploy) | us-east-1(default) | [![Launch Stack](LaunchStack.jpg)](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/template?stackName=NexusOSS&templateURL=https://aws-gcr-solutions.s3.amazonaws.com/nexus-oss-on-aws/latest/nexus-repository-oss-on-aws.template.json) 119 | AWS China(Beijing) Region | cn-north-1 | [![Launch Stack](LaunchStack.jpg)](https://console.amazonaws.cn/cloudformation/home?region=cn-north-1#/stacks/new?stackName=NexusOSS&templateURL=https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/nexus-oss-on-aws/latest/nexus-repository-oss-on-aws-cn.template.json) 120 | AWS China(Ningxia) Region | cn-northwest-1 | [![Launch Stack](LaunchStack.jpg)](https://console.amazonaws.cn/cloudformation/home?region=cn-northwest-1#/stacks/new?stackName=NexusOSS&templateURL=https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/nexus-oss-on-aws/latest/nexus-repository-oss-on-aws-cn.template.json) 121 | 122 | ### Deploy Nexus Repository OSS as an internal service inside VPC 123 | Region name | Region code | Launch 124 | --- | --- | --- 125 | Global regions(switch to the region you want to deploy) | us-east-1(default) | [![Launch Stack](LaunchStack.jpg)](https://us-east-1.console.aws.amazon.com/cloudformation/home?region=us-east-1#/stacks/create/template?stackName=PrivateNexusOSS&templateURL=https://aws-gcr-solutions.s3.amazonaws.com/nexus-oss-on-aws/latest/private-nexus-repository-oss-on-aws.template.json) 126 | AWS China(Beijing) Region | cn-north-1 | [![Launch Stack](LaunchStack.jpg)](https://console.amazonaws.cn/cloudformation/home?region=cn-north-1#/stacks/new?stackName=PrivateNexusOSS&templateURL=https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/nexus-oss-on-aws/latest/private-nexus-repository-oss-on-aws-cn.template.json) 127 | AWS China(Ningxia) Region | cn-northwest-1 | [![Launch Stack](LaunchStack.jpg)](https://console.amazonaws.cn/cloudformation/home?region=cn-northwest-1#/stacks/new?stackName=PrivateNexusOSS&templateURL=https://aws-gcr-solutions.s3.cn-north-1.amazonaws.com.cn/nexus-oss-on-aws/latest/private-nexus-repository-oss-on-aws-cn.template.json) 128 | 129 | ## Security 130 | 131 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information. 132 | 133 | ## License 134 | 135 | This library is licensed under the MIT-0 License. See the LICENSE file. 136 | 137 | Also this application uses below open source projects, 138 | 139 | - [Nexus OSS](https://github.com/sonatype/nexus-public) 140 | - [travelaudience/kubernetes-nexus](https://github.com/travelaudience/kubernetes-nexus/) 141 | - Nexus3 Helm chart in [Oteemo/charts](https://github.com/Oteemo/charts) 142 | - [AWS Load Balancer Controller](https://github.com/kubernetes-sigs/aws-load-balancer-controller) 143 | - [EKS Charts](https://github.com/aws/eks-charts) 144 | - [aws-efs-csi-driver](https://github.com/kubernetes-sigs/aws-efs-csi-driver) 145 | - [external-dns](https://github.com/kubernetes-sigs/external-dns) 146 | - [nexus3-cli](https://gitlab.com/thiagocsf/nexus3-cli) 147 | 148 | [nexus3-script]: https://help.sonatype.com/repomanager3/rest-and-integration-api/script-api 149 | [nexus-oss-on-aws-solution]: https://www.amazonaws.cn/solutions/nexusoss-on-aws/ 150 | -------------------------------------------------------------------------------- /arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nexus-oss-on-aws/2942e924c2fe6d273ebfcb85f274cad9f2167759/arch.png -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node -P tsconfig.json --prefer-ts-exts src/sonatype-nexus3.ts", 3 | "output": "cdk.out", 4 | "build": "npx projen bundle", 5 | "watch": { 6 | "include": [ 7 | "src/**/*.ts", 8 | "test/**/*.ts" 9 | ], 10 | "exclude": [ 11 | "README.md", 12 | "cdk*.json", 13 | "**/*.d.ts", 14 | "**/*.js", 15 | "tsconfig.json", 16 | "package*.json", 17 | "yarn.lock", 18 | "node_modules" 19 | ] 20 | }, 21 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 22 | } 23 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | roots: ['/test'], 3 | testMatch: ['**/*.test.ts'], 4 | transform: { 5 | '^.+\\.tsx?$': 'ts-jest' 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sonatype-nexus3", 3 | "scripts": { 4 | "build": "npx projen build", 5 | "bundle": "npx projen bundle", 6 | "clobber": "npx projen clobber", 7 | "compile": "npx projen compile", 8 | "default": "npx projen default", 9 | "deploy": "npx projen deploy", 10 | "destroy": "npx projen destroy", 11 | "diff": "npx projen diff", 12 | "eject": "npx projen eject", 13 | "eslint": "npx projen eslint", 14 | "package": "npx projen package", 15 | "post-compile": "npx projen post-compile", 16 | "post-upgrade": "npx projen post-upgrade", 17 | "pre-compile": "npx projen pre-compile", 18 | "synth": "npx projen synth", 19 | "synth:silent": "npx projen synth:silent", 20 | "test": "npx projen test", 21 | "test:watch": "npx projen test:watch", 22 | "upgrade": "npx projen upgrade", 23 | "watch": "npx projen watch", 24 | "projen": "npx projen" 25 | }, 26 | "devDependencies": { 27 | "@types/jest": "^27.5.2", 28 | "@types/node": "^14", 29 | "@typescript-eslint/eslint-plugin": "^5", 30 | "@typescript-eslint/parser": "^5", 31 | "aws-cdk": "2.37.1", 32 | "esbuild": "^0.16.17", 33 | "eslint": "^8", 34 | "eslint-import-resolver-node": "^0.3.7", 35 | "eslint-import-resolver-typescript": "^2.7.1", 36 | "eslint-plugin-import": "^2.27.4", 37 | "jest": "^27.5.1", 38 | "jest-junit": "^13", 39 | "json-schema": "^0.4.0", 40 | "lodash": ">=4.17.21", 41 | "npm-check-updates": "^16", 42 | "projen": "^0.66.12", 43 | "ts-jest": "^27.1.5", 44 | "ts-node": "^10.9.1", 45 | "typescript": "~4.6.0" 46 | }, 47 | "dependencies": { 48 | "@aws-cdk/aws-lambda-python-alpha": "^2.60.0-alpha.0", 49 | "aws-cdk-lib": "2.37.1", 50 | "constructs": "^10.0.5", 51 | "js-yaml": "^3.14.1", 52 | "sync-request": "^6.1.0" 53 | }, 54 | "keywords": [ 55 | "aws", 56 | "aws-cdk", 57 | "aws-eks", 58 | "eks", 59 | "nexus3", 60 | "sonatype" 61 | ], 62 | "license": "UNLICENSED", 63 | "version": "1.3.0-mainline", 64 | "jest": { 65 | "testMatch": [ 66 | "/src/**/__tests__/**/*.ts?(x)", 67 | "/(test|src)/**/*(*.)@(spec|test).ts?(x)" 68 | ], 69 | "clearMocks": true, 70 | "collectCoverage": true, 71 | "coverageReporters": [ 72 | "json", 73 | "lcov", 74 | "clover", 75 | "cobertura", 76 | "text" 77 | ], 78 | "coverageDirectory": "coverage", 79 | "coveragePathIgnorePatterns": [ 80 | "/node_modules/" 81 | ], 82 | "testPathIgnorePatterns": [ 83 | "/node_modules/" 84 | ], 85 | "watchPathIgnorePatterns": [ 86 | "/node_modules/" 87 | ], 88 | "reporters": [ 89 | "default", 90 | [ 91 | "jest-junit", 92 | { 93 | "outputDirectory": "test-reports" 94 | } 95 | ] 96 | ], 97 | "preset": "ts-jest", 98 | "globals": { 99 | "ts-jest": { 100 | "tsconfig": "tsconfig.dev.json" 101 | } 102 | } 103 | }, 104 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 105 | } 106 | -------------------------------------------------------------------------------- /src/lambda.d/nexus3-purge/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import time 4 | import urllib3 5 | from uuid import uuid4 6 | import os 7 | import subprocess 8 | 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | http = urllib3.PoolManager() 12 | 13 | CFN_SUCCESS = "SUCCESS" 14 | CFN_FAILED = "FAILED" 15 | 16 | # these are coming from the kubectl layer 17 | os.environ['PATH'] = '/opt/kubectl:/opt/awscli:' + os.environ['PATH'] 18 | 19 | outdir = os.environ.get('TEST_OUTDIR', '/tmp') # nosec 20 | kubeconfig = os.path.join(outdir, 'kubeconfig') 21 | 22 | def handler(event, context): 23 | 24 | def cfn_error(message=None): 25 | logger.error("| cfn_error: %s" % message) 26 | cfn_send(event, context, CFN_FAILED, reason=message) 27 | 28 | try: 29 | logger.info(event) 30 | 31 | # cloudformation request type (create/update/delete) 32 | request_type = event['RequestType'] 33 | 34 | # extract resource properties 35 | props = event['ResourceProperties'] 36 | old_props = event.get('OldResourceProperties', {}) 37 | 38 | if request_type == "Create": 39 | physical_id = f"nexus.on.aws.{str(uuid4())}" 40 | else: 41 | physical_id = event.get('PhysicalResourceId', None) 42 | if not physical_id: 43 | cfn_error("invalid request: request type is '%s' but 'PhysicalResourceId' is not defined" % request_type) 44 | return 45 | if request_type == "Delete": 46 | # resource properties (all required) 47 | cluster_name = props['ClusterName'] 48 | role_arn = props['RoleArn'] 49 | # "log in" to the cluster 50 | subprocess.check_call([ 'aws', 'eks', 'update-kubeconfig', 51 | '--role-arn', role_arn, 52 | '--name', cluster_name, 53 | '--kubeconfig', kubeconfig 54 | ]) 55 | 56 | object_type = props['ObjectType'] 57 | object_name = props['ObjectName'] 58 | object_namespace = props['ObjectNamespace'] 59 | json_path = props['JsonPath'] 60 | timeout_seconds = props['TimeoutSeconds'] 61 | relase = props['Release'] 62 | 63 | output = wait_for_purge(['get', '-n', object_namespace, object_type, object_name, "-o=jsonpath='{{{0}}}'".format(json_path)], int(timeout_seconds)) 64 | logger.info(f"The resource {object_type}/{object_name} has been purged.") 65 | 66 | try: 67 | kubectl(['delete', '-n', object_namespace, 'pvc', '-l', f'release={relase}']) 68 | logger.info(f'The PVC of helm relese {relase} is purged.') 69 | except Exception as e: 70 | error = str(e) 71 | if 'NotFound' in error or b'i/o timeout' in error: 72 | logger.warn(f"Got error '{error}'', cluster/resource might have been purged.") 73 | else: 74 | raise 75 | cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id) 76 | except KeyError as e: 77 | cfn_error(f"invalid request. Missing key {str(e)}") 78 | except subprocess.CalledProcessError as exc: 79 | errMsg = f'the cmd {exc.cmd} returns {exc.returncode} with stdout {exc.output} and stderr {exc.stderr}' 80 | logger.error(errMsg) 81 | cfn_error(errMsg) 82 | except Exception as e: 83 | logger.exception(e) 84 | cfn_error(str(e)) 85 | 86 | # sends a response to cloudformation 87 | def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId=None, noEcho=False, reason=None): 88 | 89 | responseUrl = event['ResponseURL'] 90 | logger.info(responseUrl) 91 | 92 | responseBody = {} 93 | responseBody['Status'] = responseStatus 94 | responseBody['Reason'] = reason or ('See the details in CloudWatch Log Stream: ' + context.log_stream_name) 95 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 96 | responseBody['StackId'] = event['StackId'] 97 | responseBody['RequestId'] = event['RequestId'] 98 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 99 | responseBody['NoEcho'] = noEcho 100 | responseBody['Data'] = responseData 101 | 102 | body = json.dumps(responseBody) 103 | logger.info("| response body:\n" + body) 104 | 105 | headers = { 106 | 'content-type' : '', 107 | 'content-length' : str(len(body)) 108 | } 109 | 110 | try: 111 | response = http.request('PUT', 112 | responseUrl, 113 | body=body, 114 | headers=headers, 115 | retries=False) 116 | logger.info("| status code: " + str(response.status)) 117 | except Exception as e: 118 | logger.error("| unable to send response to CloudFormation") 119 | logger.exception(e) 120 | 121 | def wait_for_purge(args, timeout_seconds): 122 | 123 | end_time = time.time() + timeout_seconds 124 | error = None 125 | 126 | while time.time() < end_time: 127 | try: 128 | # the output is surrounded with '', so we unquote 129 | output = kubectl(args).decode('utf-8')[1:-1] 130 | if output: 131 | pass 132 | except Exception as e: 133 | error = str(e) 134 | # also a recoverable error 135 | if 'NotFound' in error: 136 | return 'Resource is purged' 137 | elif b'i/o timeout' in error: 138 | logger.warn(f"Got connection error '{error}' when watching resource, ignore it") 139 | return 'Cluster might be purged' 140 | else: 141 | raise 142 | time.sleep(10) 143 | 144 | raise RuntimeError(f'Timeout waiting for output from kubectl command: {args} (last_error={error})') 145 | 146 | def kubectl(args): 147 | retry = 3 148 | while retry > 0: 149 | try: 150 | cmd = [ 'kubectl', '--kubeconfig', kubeconfig ] + args 151 | output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) 152 | except subprocess.CalledProcessError as exc: 153 | output = exc.output 154 | if b'i/o timeout' in output and retry > 0: 155 | logger.info("kubectl timed out, retries left: %s" % retry) 156 | retry = retry - 1 157 | else: 158 | raise Exception(output) 159 | else: 160 | logger.info(output) 161 | return output 162 | -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nexus-oss-on-aws/2942e924c2fe6d273ebfcb85f274cad9f2167759/src/lambda.d/nexuspreconfigure-integration-test/__init__.py -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | sys.path.append(os.path.dirname(os.path.realpath(__file__)) + "/../nexuspreconfigure") 4 | 5 | import pytest 6 | import os 7 | from subprocess import Popen, PIPE 8 | import nexuscli 9 | import nexuscli.nexus_config 10 | import nexuscli.nexus_client 11 | import nexus 12 | 13 | USERNAME = 'admin' 14 | ENDPOINT = 'http://localhost:8081/' 15 | 16 | @pytest.fixture(scope="session", autouse=True) 17 | def nexus_password(): 18 | if os.environ.get('NEXUS_PASS') is not None: 19 | return os.environ['NEXUS_PASS'] 20 | process = Popen(["docker", "exec", "nexus", "cat", "/nexus-data/admin.password"], stdout=PIPE) 21 | (output, err) = process.communicate() 22 | exit_code = process.wait() 23 | assert exit_code == 0 24 | return output 25 | 26 | @pytest.fixture(scope='session') 27 | def nexus_helper(nexus_password): 28 | nexusHelper = nexus.Nexus(username=USERNAME, 29 | password=nexus_password, endpoint=ENDPOINT) 30 | return nexusHelper 31 | 32 | @pytest.fixture(scope='session') 33 | def nexus_client(nexus_password): 34 | nexus_config = nexuscli.nexus_config.NexusConfig( 35 | username=USERNAME, password=nexus_password, url=ENDPOINT) 36 | nexus_client = nexuscli.nexus_client.NexusClient(config=nexus_config) 37 | return nexus_client -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/getBlobstores.groovy: -------------------------------------------------------------------------------- 1 | import groovy.json.* 2 | import org.sonatype.nexus.blobstore.api.* 3 | import java.util.stream.* 4 | 5 | blobStoreManager = blobStore.blobStoreManager 6 | 7 | log.debug("Getting blobstores") 8 | blobstores = new ArrayList(); 9 | blobStoreManager.browse().forEach{ 10 | blobstores.add(it) 11 | } 12 | new JsonBuilder(blobstores.stream().map{ 13 | it.getBlobStoreConfiguration().getName() 14 | }.collect(Collectors.toList())).toPrettyString() 15 | -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/requirements.txt: -------------------------------------------------------------------------------- 1 | pytest 2 | faker -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/test_nexus.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import nexuscli 3 | import nexuscli.nexus_config 4 | import nexuscli.nexus_client 5 | import nexus 6 | import pathlib 7 | import json 8 | 9 | @pytest.mark.integration 10 | def test_nexus_init(nexus_helper): 11 | """Ensure that the instantiation of Nexus""" 12 | assert nexus_helper is not None 13 | 14 | @pytest.mark.integration 15 | def test_nexus_cleanrepos(nexus_helper, nexus_client): 16 | assert len(nexus_client.repositories.raw_list()) > 0 17 | nexus_helper.deleteAllRepos() 18 | assert len(nexus_client.repositories.raw_list()) == 0 19 | 20 | @pytest.mark.integration 21 | def test_remove_default_blobstore(nexus_helper, nexus_client): 22 | scriptName = 'getBlobstores' 23 | _createScript(nexus_client, scriptName) 24 | blobs = json.loads(nexus_client.scripts.run(scriptName)['result']) 25 | assert len(blobs) == 1 26 | nexus_helper.removeDefaultFileBlobstore() 27 | blobs = json.loads(nexus_client.scripts.run(scriptName)['result']) 28 | assert len(blobs) == 0 29 | 30 | def _createScript(client, scriptName): 31 | if client.scripts.exists(scriptName): 32 | client.scripts.delete(scriptName) 33 | with open(f"{pathlib.Path(__file__).parent.absolute()}/{scriptName}.groovy") as f: 34 | scriptContent = f.read() 35 | client.scripts.create(scriptName, scriptContent) -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure-integration-test/wait-for-nexus.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | function nexus_ready { 4 | [[ "200" == $(curl -o /dev/null -s -w "%{http_code}\n" "$1") ]] 5 | } 6 | 7 | count=0 8 | until nexus_ready "${1:-http://localhost:8081}" 9 | do 10 | count=$((count+1)) 11 | if [ ${count} -gt 100 ] 12 | then 13 | echo 'Timeout-out waiting for nexus container' 14 | docker logs --tail 50 nexus 15 | docker ps 16 | curl -sv "%{http_code}\n" "$1" 17 | netstat -ntlp 18 | exit 1 19 | fi 20 | sleep 5 21 | done 22 | -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/nexus-oss-on-aws/2942e924c2fe6d273ebfcb85f274cad9f2167759/src/lambda.d/nexuspreconfigure/__init__.py -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/createBlobstore.groovy: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonSlurper 2 | import org.sonatype.nexus.repository.config.Configuration 3 | import groovy.transform.ToString 4 | 5 | @ToString 6 | class Blobstore { 7 | String name 8 | String type 9 | Map config 10 | } 11 | 12 | blobStoreManager = blobStore.blobStoreManager 13 | 14 | if (args != "") { 15 | log.info("Creating blobstore with args [${args}]") 16 | def blobstore = convertJsonFileToBlobstore(args) 17 | log.info("Got blobstore [${blobstore}]") 18 | validateBlobstore(blobstore) 19 | createBlobstore(blobstore) 20 | } 21 | 22 | def validateBlobstore(Blobstore blobstore) { 23 | if (blobstore.name == null) 24 | throw new IllegalArgumentException("The name of blobstore is required.") 25 | if (blobstore.type == null) 26 | throw new IllegalArgumentException("The type of blobstore is required.") 27 | if (blobstore.type == "s3" && (blobstore.config == null || !blobstore.config.containsKey('bucket'))) 28 | throw new IllegalArgumentException("The bucket config of s3 blobstore is required.") 29 | } 30 | 31 | def createBlobstore(Blobstore blobstore) { 32 | if(!blobStoreManager.get(blobstore.name)) { 33 | if (blobstore.type == 's3') 34 | blobStore.createS3BlobStore(blobstore.name, blobstore.config) 35 | else 36 | new UnsupportedOperationException(blobstore.type + " is not supported") 37 | } else { 38 | return "already exists" 39 | } 40 | "success" 41 | } 42 | 43 | def convertJsonFileToBlobstore(String jsonData) { 44 | def inputJson = new JsonSlurper().parseText(jsonData) 45 | log.debug("Creating blobstore object for [${inputJson}]") 46 | Blobstore blobstore = new Blobstore() 47 | inputJson.each { 48 | if (it.key == 'name') 49 | blobstore.name = it.value 50 | else if (it.key == 'type') 51 | blobstore.type = it.value 52 | else if (it.key == 'config') 53 | blobstore.config = it.value 54 | } 55 | 56 | log.debug("Created blobstore object [${blobstore}]") 57 | blobstore 58 | } 59 | -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/deleteDefaultBlobstore.groovy: -------------------------------------------------------------------------------- 1 | 2 | blobStoreManager = blobStore.blobStoreManager 3 | 4 | void deleteDefaultBlobStores() { 5 | List stores = blobStoreManager.browse()*.blobStoreConfiguration*.name 6 | stores.findAll { 7 | it == "default" 8 | }.each { 9 | blobStoreManager.delete(it) 10 | } 11 | } 12 | 13 | deleteDefaultBlobStores() 14 | 'success' -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/index.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import urllib3 4 | from uuid import uuid4 5 | import nexus 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | http = urllib3.PoolManager() 10 | 11 | CFN_SUCCESS = "SUCCESS" 12 | CFN_FAILED = "FAILED" 13 | 14 | def handler(event, context): 15 | 16 | def cfn_error(message=None): 17 | logger.error("| cfn_error: %s" % message) 18 | cfn_send(event, context, CFN_FAILED, reason=message) 19 | 20 | try: 21 | logger.info(event) 22 | 23 | # cloudformation request type (create/update/delete) 24 | request_type = event['RequestType'] 25 | 26 | # extract resource properties 27 | props = event['ResourceProperties'] 28 | old_props = event.get('OldResourceProperties', {}) 29 | 30 | if request_type == "Create": 31 | physical_id = f"nexus.on.aws.{str(uuid4())}" 32 | else: 33 | physical_id = event.get('PhysicalResourceId', None) 34 | if not physical_id: 35 | cfn_error("invalid request: request type is '%s' but 'PhysicalResourceId' is not defined" % request_type) 36 | return 37 | if request_type != "Delete": 38 | username = props['Username'] 39 | password = props['Password'] 40 | endpoint = props['Endpoint'] 41 | blobstoreName = props['BlobStoreName'] if 'BlobStoreName' in props else 's3-blobsstore' 42 | bucketName = props['S3BucketName'] 43 | nexusHelper = nexus.Nexus(username=username, password=password, endpoint=endpoint) 44 | nexusHelper.deleteAllRepos() 45 | nexusHelper.removeDefaultFileBlobstore() 46 | nexusHelper.createS3Blobstore(blobstoreName, bucketName, '-1') 47 | cfn_send(event, context, CFN_SUCCESS, physicalResourceId=physical_id) 48 | except KeyError as e: 49 | cfn_error(f"invalid request. Missing key {str(e)}") 50 | except Exception as e: 51 | logger.exception(str(e)) 52 | cfn_error(str(e)) 53 | 54 | # sends a response to cloudformation 55 | def cfn_send(event, context, responseStatus, responseData={}, physicalResourceId=None, noEcho=False, reason=None): 56 | 57 | responseUrl = event['ResponseURL'] 58 | logger.info(responseUrl) 59 | 60 | responseBody = {} 61 | responseBody['Status'] = responseStatus 62 | responseBody['Reason'] = reason or ('See the details in CloudWatch Log Stream: ' + context.log_stream_name) 63 | responseBody['PhysicalResourceId'] = physicalResourceId or context.log_stream_name 64 | responseBody['StackId'] = event['StackId'] 65 | responseBody['RequestId'] = event['RequestId'] 66 | responseBody['LogicalResourceId'] = event['LogicalResourceId'] 67 | responseBody['NoEcho'] = noEcho 68 | responseBody['Data'] = responseData 69 | 70 | body = json.dumps(responseBody) 71 | logger.info("| response body:\n" + body) 72 | 73 | headers = { 74 | 'content-type' : '', 75 | 'content-length' : str(len(body)) 76 | } 77 | 78 | try: 79 | response = http.request('PUT', 80 | responseUrl, 81 | body=body, 82 | headers=headers, 83 | retries=False) 84 | logger.info("| status code: " + str(response.status)) 85 | except Exception as e: 86 | logger.error("| unable to send response to CloudFormation") 87 | logger.exception(e) -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/nexus.py: -------------------------------------------------------------------------------- 1 | import nexuscli 2 | import nexuscli.nexus_config 3 | import nexuscli.nexus_client 4 | import logging 5 | import pathlib 6 | import json 7 | 8 | # logging.basicConfig(level=logging.INFO) 9 | logger = logging.getLogger() 10 | logger.setLevel(logging.INFO) 11 | 12 | class Nexus: 13 | def __init__(self, username, password, endpoint): 14 | nexus_config = nexuscli.nexus_config.NexusConfig( 15 | username=username, password=password, url=endpoint) 16 | self.nexus_client = nexuscli.nexus_client.NexusClient(config=nexus_config) 17 | 18 | def deleteAllRepos(self): 19 | self.nexus_client.repositories.refresh() 20 | repositories = self.nexus_client.repositories.raw_list() 21 | for repo in repositories: 22 | logger.info(f"""| Nexus: deleting repo with name '{repo['name']}', 23 | format '{repo['format']}' and type '{repo['type']}""") 24 | self.nexus_client.repositories.delete(repo['name']) 25 | 26 | def removeDefaultFileBlobstore(self): 27 | logger.info(f"| Nexus: deleting default file blobstore") 28 | scriptName = "deleteDefaultBlobstore" 29 | self._createScript(self.nexus_client, scriptName) 30 | self.nexus_client.scripts.run(scriptName) 31 | 32 | def _createScript(self, client, scriptName): 33 | if client.scripts.exists(scriptName): 34 | logger.warn(f"| Nexus: deleting existing script {scriptName} for recreating a new one") 35 | client.scripts.delete(scriptName) 36 | with open(f"{pathlib.Path(__file__).parent.absolute()}/{scriptName}.groovy") as f: 37 | scriptContent = f.read() 38 | logger.info(f"| Nexus: creating script {scriptName}") 39 | client.scripts.create(scriptName, scriptContent) 40 | 41 | def createS3Blobstore(self, blobName, s3Bucket, expiration): 42 | logger.info(f"""| Nexus: creating s3 file blobstore with name {blobName}, 43 | bucket {s3Bucket} and expiration {expiration}""") 44 | scriptName = "createBlobstore" 45 | self._createScript(self.nexus_client, scriptName) 46 | resp = self.nexus_client.scripts.run(scriptName, json.dumps({ 47 | 'name': blobName, 48 | 'type': 's3', 49 | 'config': { 50 | 'expiration': expiration, 51 | 'bucket': s3Bucket, 52 | } 53 | })) 54 | 55 | if resp['result'] == "already exists": 56 | logger.warn(f"| Nexus: creating existing bucket '{blobName}'!") 57 | logger.info(f"| Nexus: created bucket '{blobName}' with response {resp['result']}") -------------------------------------------------------------------------------- /src/lambda.d/nexuspreconfigure/requirements.txt: -------------------------------------------------------------------------------- 1 | nexus3-cli == 3.4.0 -------------------------------------------------------------------------------- /src/lambda.d/setup.cfg: -------------------------------------------------------------------------------- 1 | [tool:pytest] 2 | addopts = -v --color=yes 3 | testpaths = nexuspreconfigure-integration-test 4 | junit_family = xunit2 5 | markers = 6 | integration: test against a real Nexus instance -------------------------------------------------------------------------------- /src/lib/sonatype-nexus3-stack.ts: -------------------------------------------------------------------------------- 1 | /* eslint @typescript-eslint/no-require-imports: "off" */ 2 | import * as path from 'path'; 3 | import * as lambda_python from '@aws-cdk/aws-lambda-python-alpha'; 4 | import * as cdk from 'aws-cdk-lib'; 5 | import * as certmgr from 'aws-cdk-lib/aws-certificatemanager'; 6 | import * as ec2 from 'aws-cdk-lib/aws-ec2'; 7 | import * as efs from 'aws-cdk-lib/aws-efs'; 8 | import * as eks from 'aws-cdk-lib/aws-eks'; 9 | import * as iam from 'aws-cdk-lib/aws-iam'; 10 | import * as lambda from 'aws-cdk-lib/aws-lambda'; 11 | import * as logs from 'aws-cdk-lib/aws-logs'; 12 | import * as route53 from 'aws-cdk-lib/aws-route53'; 13 | import * as s3 from 'aws-cdk-lib/aws-s3'; 14 | import { AwsCliLayer } from 'aws-cdk-lib/lambda-layer-awscli'; 15 | import { KubectlLayer } from 'aws-cdk-lib/lambda-layer-kubectl'; 16 | import { IConstruct, Construct } from 'constructs'; 17 | // @ts-ignore 18 | import * as pjson from '../../package.json'; 19 | 20 | const assert = require('assert').strict; 21 | 22 | export class SonatypeNexus3Stack extends cdk.Stack { 23 | 24 | constructor(scope: Construct, id: string, props?: cdk.StackProps) { 25 | super(scope, id, props); 26 | 27 | const targetRegion = this.node.tryGetContext('region') ?? 'us-east-1'; 28 | 29 | const partitionMapping = new cdk.CfnMapping(this, 'PartitionMapping', { 30 | mapping: { 31 | 'aws': { 32 | nexus: 'quay.io/travelaudience/docker-nexus', 33 | nexusProxy: 'quay.io/travelaudience/docker-nexus-proxy', 34 | albHelmChartRepo: 'https://aws.github.io/eks-charts', 35 | efsCSIHelmChartRepo: 'https://kubernetes-sigs.github.io/aws-efs-csi-driver/', 36 | nexusHelmChartRepo: 'https://oteemo.github.io/charts/', 37 | }, 38 | 'aws-cn': { 39 | nexus: '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/quay/travelaudience/docker-nexus', 40 | nexusProxy: '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/quay/travelaudience/docker-nexus-proxy', 41 | albHelmChartRepo: 'https://aws-gcr-solutions-assets.s3.cn-northwest-1.amazonaws.com.cn/helm/charts/eks-charts/', 42 | efsCSIHelmChartRepo: 'https://aws-gcr-solutions-assets.s3.cn-northwest-1.amazonaws.com.cn/helm/charts/aws-efs-csi-driver/', 43 | nexusHelmChartRepo: 'https://aws-gcr-solutions-assets.s3.cn-northwest-1.amazonaws.com.cn/helm/charts/oteemo/', 44 | }, 45 | }, 46 | }); 47 | 48 | const constraintDescription = '- at least 8 characters\n- must contain at least 1 uppercase letter, 1 lowercase letter, and 1 number\n- Can contain special characters'; 49 | const adminInitPassword = new cdk.CfnParameter(this, 'NexusAdminInitPassword', { 50 | type: 'String', 51 | allowedPattern: '^(?=.*\\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[a-zA-Z]).{8,}$', 52 | minLength: 8, 53 | description: `The admin init password of Nexus3. ${constraintDescription}`, 54 | constraintDescription, 55 | noEcho: true, 56 | }); 57 | var hostedZone = null; 58 | var certificate: certmgr.Certificate | undefined; 59 | var domainName: string | undefined; 60 | 61 | const internalALB = (/true/i).test(this.node.tryGetContext('internalALB')); 62 | const r53Domain = internalALB ? undefined : this.node.tryGetContext('r53Domain'); 63 | 64 | if (!internalALB) { 65 | const domainNameParameter = new cdk.CfnParameter(this, 'DomainName', { 66 | type: 'String', 67 | allowedPattern: '(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]', 68 | description: 'The domain name of Nexus OSS deployment, such as mydomain.com.', 69 | constraintDescription: 'validate domain name without protocol', 70 | }); 71 | domainName = domainNameParameter.valueAsString; 72 | if (r53Domain) { 73 | hostedZone = route53.HostedZone.fromLookup(this, 'R53HostedZone', { 74 | domainName: r53Domain, 75 | privateZone: false, 76 | }); 77 | assert.ok(hostedZone != null, 'Can not find your hosted zone.'); 78 | certificate = new certmgr.Certificate(this, 'SSLCertificate', { 79 | domainName: domainName, 80 | validation: certmgr.CertificateValidation.fromDns(hostedZone), 81 | }); 82 | } else if ((/true/i).test(this.node.tryGetContext('enableR53HostedZone'))) { 83 | const r53HostedZoneIdParameter = new cdk.CfnParameter(this, 'R53HostedZoneId', { 84 | type: 'AWS::Route53::HostedZone::Id', 85 | description: 'The hosted zone ID of given domain name in Route 53.', 86 | }); 87 | hostedZone = route53.HostedZone.fromHostedZoneId(this, 'ImportedHostedZone', r53HostedZoneIdParameter.valueAsString); 88 | certificate = new certmgr.Certificate(this, 'SSLCertificate', { 89 | domainName: domainName, 90 | validation: certmgr.CertificateValidation.fromDns(hostedZone), 91 | }); 92 | } 93 | } 94 | 95 | const logBucket = new s3.Bucket(this, 'LogBucket', { 96 | encryption: s3.BucketEncryption.S3_MANAGED, 97 | removalPolicy: cdk.RemovalPolicy.RETAIN, 98 | serverAccessLogsPrefix: 'logBucketAccessLog', 99 | }); 100 | 101 | const vpcId = this.node.tryGetContext('vpcId'); 102 | const vpc = vpcId ? ec2.Vpc.fromLookup(this, 'NexusOSSVpc', { 103 | vpcId: vpcId === 'default' ? undefined : vpcId, 104 | isDefault: vpcId === 'default' ? true : undefined, 105 | }) : (() => { 106 | const newVpc = new ec2.Vpc(this, 'NexusOSSVpc', { 107 | maxAzs: 2, 108 | }); 109 | const flowLogPrefix = 'vpcFlowLogs'; 110 | newVpc.addFlowLog('VpcFlowlogs', { 111 | destination: ec2.FlowLogDestination.toS3(logBucket, flowLogPrefix), 112 | }); 113 | // https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs-s3.html#flow-logs-s3-permissions 114 | logBucket.addToResourcePolicy(new iam.PolicyStatement({ 115 | sid: 'AWSLogDeliveryWrite', 116 | principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], 117 | actions: ['s3:PutObject'], 118 | resources: [logBucket.arnForObjects(`${flowLogPrefix}/AWSLogs/${cdk.Aws.ACCOUNT_ID}/*`)], 119 | conditions: { 120 | StringEquals: { 121 | 's3:x-amz-acl': 'bucket-owner-full-control', 122 | }, 123 | }, 124 | })); 125 | logBucket.addToResourcePolicy(new iam.PolicyStatement({ 126 | sid: 'AWSLogDeliveryCheck', 127 | principals: [new iam.ServicePrincipal('delivery.logs.amazonaws.com')], 128 | actions: [ 129 | 's3:GetBucketAcl', 130 | 's3:ListBucket', 131 | ], 132 | resources: [logBucket.bucketArn], 133 | })); 134 | return newVpc; 135 | })(); 136 | if (this.azOfSubnets(vpc.publicSubnets) <= 1 || 137 | this.azOfSubnets(vpc.privateSubnets) <= 1) { 138 | throw new Error(`VPC '${vpc.vpcId}' must have both public and private subnets cross two AZs at least.`); 139 | } 140 | 141 | const request = require('sync-request'); 142 | const yaml = require('js-yaml'); 143 | 144 | const importedEks = this.node.tryGetContext('importedEKS') ?? false; 145 | var cluster: eks.ICluster; 146 | var nodeGroup: eks.Nodegroup; 147 | var eksVersion: cdk.CfnParameter; 148 | 149 | const nexusBlobBucket = new s3.Bucket(this, 'nexus3-blobstore', { 150 | removalPolicy: cdk.RemovalPolicy.RETAIN, 151 | encryption: s3.BucketEncryption.S3_MANAGED, 152 | serverAccessLogsBucket: logBucket, 153 | serverAccessLogsPrefix: 'blobstoreBucketAccessLog', 154 | enforceSSL: true, 155 | }); 156 | if (vpc instanceof ec2.Vpc) { 157 | const gatewayEndpoint = vpc.addGatewayEndpoint('s3', { 158 | service: ec2.GatewayVpcEndpointAwsService.S3, 159 | }); 160 | nexusBlobBucket.addToResourcePolicy(new iam.PolicyStatement({ 161 | effect: iam.Effect.DENY, 162 | actions: ['s3:*'], 163 | principals: [new iam.AccountPrincipal(cdk.Aws.ACCOUNT_ID)], 164 | resources: [ 165 | nexusBlobBucket.bucketArn, 166 | nexusBlobBucket.arnForObjects('*'), 167 | ], 168 | conditions: { 169 | StringNotEquals: { 170 | 'aws:SourceVpce': gatewayEndpoint.vpcEndpointId, 171 | }, 172 | }, 173 | })); 174 | } 175 | const s3BucketPolicy = new iam.PolicyStatement({ 176 | effect: iam.Effect.ALLOW, 177 | actions: [ 178 | 's3:ListBucket', 179 | ], 180 | resources: [nexusBlobBucket.bucketArn], 181 | }); 182 | const s3ObjectPolicy = new iam.PolicyStatement({ 183 | effect: iam.Effect.ALLOW, 184 | actions: [ 185 | 's3:GetBucketAcl', 186 | 's3:PutObject', 187 | 's3:GetObject', 188 | 's3:DeleteObject', 189 | 's3:PutObjectTagging', 190 | 's3:GetObjectTagging', 191 | 's3:DeleteObjectTagging', 192 | 's3:GetLifecycleConfiguration', 193 | 's3:PutLifecycleConfiguration', 194 | ], 195 | resources: [ 196 | nexusBlobBucket.bucketArn, 197 | nexusBlobBucket.arnForObjects('*'), 198 | ], 199 | }); 200 | 201 | if (importedEks) { 202 | if (!vpcId) { 203 | throw new Error('Context variable "vpcId" must be specified for imported EKS cluster.'); 204 | } 205 | 206 | const clusterName = this.node.tryGetContext('eksClusterName'); 207 | const kubectlRoleArn = this.node.tryGetContext('eksKubectlRoleArn'); 208 | const openIdConnectProviderArn = this.node.tryGetContext('eksOpenIdConnectProviderArn'); 209 | const nodeGroupRoleArn = this.node.tryGetContext('nodeGroupRoleArn'); 210 | 211 | if (!clusterName || !kubectlRoleArn || !openIdConnectProviderArn || !nodeGroupRoleArn) { 212 | throw new Error('Context variables "eksClusterName", "eksKubectlRoleArn", "eksOpenIdConnectProviderArn", "nodeGroupRoleArn" must be specified for imported EKS cluster.'); 213 | } 214 | 215 | cluster = eks.Cluster.fromClusterAttributes(this, 'ImportedEKS', { 216 | clusterName, 217 | kubectlRoleArn, 218 | openIdConnectProvider: eks.OpenIdConnectProvider.fromOpenIdConnectProviderArn( 219 | this, 'ImportedClusterOpendIdConnectProvider', openIdConnectProviderArn), 220 | vpc, 221 | }); 222 | // the limitation of Nexus3 image not working with IRSA 223 | const nodeGroupRole = iam.Role.fromRoleArn(this, 'NodeGroupRole', nodeGroupRoleArn, { 224 | mutable: true, 225 | }); 226 | nodeGroupRole.attachInlinePolicy(new iam.Policy(this, 'NexusS3BlobStore', { 227 | statements: [s3BucketPolicy, s3ObjectPolicy], 228 | })); 229 | } else { 230 | const clusterAdmin = new iam.Role(this, 'AdminRole', { 231 | assumedBy: new iam.AccountRootPrincipal(), 232 | }); 233 | 234 | const isFargetEnabled = (this.node.tryGetContext('enableFarget') || 'false').toLowerCase() === 'true'; 235 | 236 | eksVersion = new cdk.CfnParameter(this, 'KubernetesVersion', { 237 | type: 'String', 238 | allowedValues: [ 239 | '1.22', 240 | '1.21', 241 | '1.20', 242 | ], 243 | default: '1.20', 244 | description: 'The version of Kubernetes.', 245 | }); 246 | 247 | cluster = new eks.Cluster(this, 'NexusCluster', { 248 | vpc, 249 | endpointAccess: eks.EndpointAccess.PRIVATE, 250 | defaultCapacity: 0, 251 | mastersRole: clusterAdmin, 252 | version: eks.KubernetesVersion.of(eksVersion.valueAsString), 253 | coreDnsComputeType: isFargetEnabled ? eks.CoreDnsComputeType.FARGATE : eks.CoreDnsComputeType.EC2, 254 | clusterLogging: [ 255 | eks.ClusterLoggingTypes.API, 256 | eks.ClusterLoggingTypes.AUTHENTICATOR, 257 | eks.ClusterLoggingTypes.SCHEDULER, 258 | ], 259 | }); 260 | 261 | if (isFargetEnabled) { 262 | (cluster as eks.Cluster).addFargateProfile('FargetProfile', { 263 | selectors: [ 264 | { 265 | namespace: 'kube-system', 266 | labels: { 267 | 'k8s-app': 'kube-dns', 268 | }, 269 | }, 270 | ], 271 | }); 272 | } 273 | 274 | const template = new ec2.LaunchTemplate(this, 'EKSManagedNodeTemplate', { 275 | blockDevices: [ 276 | { 277 | deviceName: '/dev/xvda', 278 | volume: ec2.BlockDeviceVolume.ebs(30, { 279 | encrypted: true, 280 | }), 281 | }, 282 | ], 283 | detailedMonitoring: true, 284 | }); 285 | nodeGroup = (cluster as eks.Cluster).addNodegroupCapacity('nodegroup', { 286 | nodegroupName: 'nexus3', 287 | instanceTypes: [ 288 | new ec2.InstanceType(this.node.tryGetContext('instanceType') ?? 'm5.large'), 289 | ], 290 | minSize: 1, 291 | maxSize: 3, 292 | launchTemplateSpec: { 293 | id: template.launchTemplateId!, 294 | }, 295 | labels: { 296 | usage: 'nexus3', 297 | }, 298 | }); 299 | // Have to bind IAM role to node due to Nexus3 uses old AWS Java SDK not supporting IRSA 300 | // see https://github.com/sonatype/nexus-public/pull/69 for detail 301 | nodeGroup.role.attachInlinePolicy(new iam.Policy(this, 'NexusS3BlobStore', { 302 | statements: [s3BucketPolicy, s3ObjectPolicy], 303 | })); 304 | 305 | // install SSM agent as daemonset 306 | nodeGroup.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName('AmazonSSMManagedInstanceCore')); 307 | 308 | (cluster.node.findChild('Resource').node.findChild('CreationRole').node.findChild('DefaultPolicy') 309 | .node.findChild('Resource') as cdk.CfnResource).addMetadata('cfn_nag', { 310 | rules_to_suppress: [ 311 | { 312 | id: 'W12', 313 | reason: 'wildcard in policy is built by CDK', 314 | }, 315 | ], 316 | }); 317 | 318 | var ssmManifests = request('GET', 'https://raw.githubusercontent.com/aws-samples/ssm-agent-daemonset-installer/541da0a68a96d5b2ce184724f3d35d22d9ac7236/setup.yaml') 319 | .getBody('utf-8'); 320 | 321 | if (targetRegion.startsWith('cn-')) { 322 | ssmManifests = ssmManifests.replace('https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm', 323 | 'https://s3.cn-north-1.amazonaws.com.cn/amazon-ssm-cn-north-1/latest/linux_amd64/amazon-ssm-agent.rpm') 324 | .replace('jicowan/ssm-agent-installer:1.2', '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/dockerhub/jicowan/ssm-agent-installer:1.2') 325 | .replace('gcr.io/google-containers/pause:2.0', '048912060910.dkr.ecr.cn-northwest-1.amazonaws.com.cn/gcr/google-containers/pause:2.0'); 326 | } 327 | const ssmInstallerResources = yaml.safeLoadAll(ssmManifests); 328 | cluster.addManifest('ssm-agent-daemonset', ...ssmInstallerResources); 329 | } 330 | 331 | // install AWS load balancer via Helm charts 332 | const awsLoadBalancerControllerVersion = 'v2.4.3'; 333 | const awsControllerBaseResourceBaseUrl = `https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/${awsLoadBalancerControllerVersion}/docs`; 334 | const awsControllerPolicyUrl = `${awsControllerBaseResourceBaseUrl}/install/iam_policy${targetRegion.startsWith('cn-') ? '_cn' : ''}.json`; 335 | const albNamespace = 'kube-system'; 336 | const albServiceAccount = cluster.addServiceAccount('aws-load-balancer-controller', { 337 | name: 'aws-load-balancer-controller', 338 | namespace: albNamespace, 339 | }); 340 | const customResourceRole = cdk.Stack.of(this).node.tryFindChild('Custom::AWSCDKOpenIdConnectProviderCustomResourceProvider'); 341 | if (customResourceRole) { 342 | (customResourceRole.node.findChild('Role') as cdk.CfnResource).addMetadata('cfn_nag', { 343 | rules_to_suppress: [ 344 | { 345 | id: 'W11', 346 | reason: 'wildcard in policy built by CDK', 347 | }, 348 | ], 349 | }); 350 | } 351 | 352 | const policyJson = request('GET', awsControllerPolicyUrl).getBody(); 353 | ((JSON.parse(policyJson)).Statement as []).forEach((statement, _idx, _array) => { 354 | albServiceAccount.addToPrincipalPolicy(iam.PolicyStatement.fromJson(statement)); 355 | }); 356 | const albSAPolicy = albServiceAccount.role.node.children.filter(c => c instanceof iam.Policy)[0].node.defaultChild as iam.CfnPolicy; 357 | albSAPolicy.addMetadata('cfn_nag', { 358 | rules_to_suppress: [ 359 | { 360 | id: 'W76', 361 | reason: 'the policy statement is from official doc of AWS load balancer controller', 362 | }, 363 | { 364 | id: 'W12', 365 | reason: 'the policy statement is from official doc of AWS load balancer controller', 366 | }, 367 | ], 368 | }); 369 | const awsLoadBalancerControllerChart = cluster.addHelmChart('AWSLoadBalancerController', { 370 | chart: 'aws-load-balancer-controller', 371 | repository: partitionMapping.findInMap(cdk.Aws.PARTITION, 'albHelmChartRepo'), 372 | namespace: albNamespace, 373 | release: 'aws-load-balancer-controller', 374 | version: '1.4.4', // mapping to v2.4.3 375 | wait: true, 376 | timeout: cdk.Duration.minutes(15), 377 | values: { 378 | clusterName: cluster.clusterName, 379 | image: { 380 | repository: this.getAwsLoadBalancerControllerRepo(), 381 | }, 382 | serviceAccount: { 383 | create: false, 384 | name: albServiceAccount.serviceAccountName, 385 | }, 386 | // must disable waf features for aws-cn partition 387 | enableShield: false, 388 | enableWaf: false, 389 | enableWafv2: false, 390 | }, 391 | }); 392 | 393 | if (cluster instanceof eks.Cluster) { 394 | awsLoadBalancerControllerChart.node.addDependency(nodeGroup!); 395 | awsLoadBalancerControllerChart.node.addDependency(cluster.awsAuth); 396 | } 397 | awsLoadBalancerControllerChart.node.addDependency(albServiceAccount); 398 | awsLoadBalancerControllerChart.node.addDependency(cluster.openIdConnectProvider); 399 | 400 | // deploy EFS, EFS CSI driver, PV 401 | const efsCSI = cluster.addHelmChart('EFSCSIDriver', { 402 | chart: 'aws-efs-csi-driver', 403 | repository: partitionMapping.findInMap(cdk.Aws.PARTITION, 'efsCSIHelmChartRepo'), 404 | release: 'aws-efs-csi-driver', 405 | version: '2.2.0', // mapping to v1.3.4 406 | }); 407 | if (cluster instanceof eks.Cluster) { 408 | efsCSI.node.addDependency(nodeGroup!); 409 | efsCSI.node.addDependency(cluster.awsAuth); 410 | } 411 | efsCSI.node.addDependency(cluster.openIdConnectProvider); 412 | 413 | const fileSystem = new efs.FileSystem(this, 'Nexus3FileSystem', { 414 | vpc, 415 | encrypted: true, 416 | performanceMode: efs.PerformanceMode.GENERAL_PURPOSE, 417 | throughputMode: efs.ThroughputMode.BURSTING, 418 | }); 419 | fileSystem.connections.allowDefaultPortFrom(ec2.Peer.ipv4(vpc.vpcCidrBlock), 420 | 'allow access efs from inside vpc'); 421 | fileSystem.connections.securityGroups.forEach(sg => 422 | (sg.node.defaultChild as ec2.CfnSecurityGroup).applyRemovalPolicy(cdk.RemovalPolicy.DESTROY)); 423 | const efsClass = 'efs-sc'; 424 | const efsStorageClass = cluster.addManifest('efs-storageclass', 425 | { 426 | kind: 'StorageClass', 427 | apiVersion: 'storage.k8s.io/v1', 428 | metadata: { 429 | name: efsClass, 430 | }, 431 | provisioner: 'efs.csi.aws.com', 432 | }); 433 | efsStorageClass.node.addDependency(efsCSI); 434 | const efsPVName = 'nexus3-oss-efs-pv'; 435 | const efsPV = cluster.addManifest('efs-pv', { 436 | apiVersion: 'v1', 437 | kind: 'PersistentVolume', 438 | metadata: { 439 | name: efsPVName, 440 | }, 441 | spec: { 442 | capacity: { 443 | storage: '1000Gi', 444 | }, 445 | volumeMode: 'Filesystem', 446 | accessModes: [ 447 | 'ReadWriteMany', 448 | ], 449 | persistentVolumeReclaimPolicy: 'Retain', 450 | storageClassName: efsClass, 451 | csi: { 452 | driver: 'efs.csi.aws.com', 453 | volumeHandle: fileSystem.fileSystemId, 454 | }, 455 | }, 456 | }); 457 | efsPV.node.addDependency(fileSystem); 458 | efsPV.node.addDependency(efsStorageClass); 459 | 460 | const nexus3Namespace = 'default'; 461 | const nexus3ChartName = 'nexus3'; 462 | const nexusServiceAccount = cluster.addServiceAccount('sonatype-nexus3', { 463 | name: 'sonatype-nexus3', 464 | namespace: nexus3Namespace, 465 | }); 466 | 467 | nexusServiceAccount.addToPrincipalPolicy(s3BucketPolicy); 468 | nexusServiceAccount.addToPrincipalPolicy(s3ObjectPolicy); 469 | 470 | const albLogServiceAccountMapping = new cdk.CfnMapping(this, 'ALBServiceAccountMapping', { 471 | mapping: { 472 | 'me-south-1': { 473 | account: '076674570225', 474 | }, 475 | 'eu-south-1': { 476 | account: '635631232127', 477 | }, 478 | 'ap-northeast-1': { 479 | account: '582318560864', 480 | }, 481 | 'ap-northeast-2': { 482 | account: '600734575887', 483 | }, 484 | 'ap-northeast-3': { 485 | account: '383597477331', 486 | }, 487 | 'ap-south-1': { 488 | account: '718504428378', 489 | }, 490 | 'ap-southeast-1': { 491 | account: '114774131450', 492 | }, 493 | 'ap-southeast-2': { 494 | account: '783225319266', 495 | }, 496 | 'ca-central-1': { 497 | account: '985666609251', 498 | }, 499 | 'eu-central-1': { 500 | account: '054676820928', 501 | }, 502 | 'eu-north-1': { 503 | account: '897822967062', 504 | }, 505 | 'eu-west-1': { 506 | account: '156460612806', 507 | }, 508 | 'eu-west-2': { 509 | account: '652711504416', 510 | }, 511 | 'eu-west-3': { 512 | account: '009996457667', 513 | }, 514 | 'sa-east-1': { 515 | account: '507241528517', 516 | }, 517 | 'us-east-1': { 518 | account: '127311923021', 519 | }, 520 | 'us-east-2': { 521 | account: '033677994240', 522 | }, 523 | 'us-west-1': { 524 | account: '027434742980', 525 | }, 526 | 'us-west-2': { 527 | account: '797873946194', 528 | }, 529 | 'ap-east-1': { 530 | account: '754344448648', 531 | }, 532 | 'af-south-1': { 533 | account: '098369216593', 534 | }, 535 | 'cn-north-1': { 536 | account: '638102146993', 537 | }, 538 | 'cn-northwest-1': { 539 | account: '037604701340', 540 | }, 541 | }, 542 | }); 543 | const albLogPrefix = 'albAccessLog'; 544 | logBucket.grantPut(new iam.AccountPrincipal(albLogServiceAccountMapping.findInMap(cdk.Aws.REGION, 'account')), 545 | `${albLogPrefix}/AWSLogs/${cdk.Aws.ACCOUNT_ID}/*`); 546 | 547 | const nexusPort = 8081; 548 | const healthcheckPath = '/'; 549 | var albOptions = { 550 | 'alb.ingress.kubernetes.io/backend-protocol': 'HTTP', 551 | 'alb.ingress.kubernetes.io/healthcheck-path': healthcheckPath, 552 | 'alb.ingress.kubernetes.io/healthcheck-port': nexusPort, 553 | 'alb.ingress.kubernetes.io/listen-ports': '[{"HTTP": 80}]', 554 | 'alb.ingress.kubernetes.io/scheme': internalALB ? 'internal' : 'internet-facing', 555 | 'alb.ingress.kubernetes.io/inbound-cidrs': internalALB ? vpc.vpcCidrBlock : '0.0.0.0/0', 556 | 'alb.ingress.kubernetes.io/auth-type': 'none', 557 | 'alb.ingress.kubernetes.io/target-type': 'ip', 558 | 'kubernetes.io/ingress.class': 'alb', 559 | 'alb.ingress.kubernetes.io/tags': 'app=nexus3', 560 | 'alb.ingress.kubernetes.io/subnets': vpc.publicSubnets.map(subnet => subnet.subnetId).join(','), 561 | 'alb.ingress.kubernetes.io/load-balancer-attributes': `access_logs.s3.enabled=true,access_logs.s3.bucket=${logBucket.bucketName},access_logs.s3.prefix=${albLogPrefix}`, 562 | }; 563 | const ingressRules: Array = [ 564 | { 565 | http: { 566 | paths: [ 567 | { 568 | path: '/', 569 | pathType: 'Prefix', 570 | backend: { 571 | service: { 572 | name: `${nexus3ChartName}-sonatype-nexus`, 573 | port: { 574 | number: nexusPort, 575 | }, 576 | }, 577 | }, 578 | }, 579 | ], 580 | }, 581 | }, 582 | ]; 583 | var externalDNSResource: Construct; 584 | if (certificate) { 585 | Object.assign(albOptions, { 586 | 'alb.ingress.kubernetes.io/certificate-arn': certificate.certificateArn, 587 | 'alb.ingress.kubernetes.io/ssl-policy': 'ELBSecurityPolicy-TLS-1-2-Ext-2018-06', 588 | 'alb.ingress.kubernetes.io/listen-ports': '[{"HTTP": 80}, {"HTTPS": 443}]', 589 | 'alb.ingress.kubernetes.io/actions.ssl-redirect': '{"type": "redirect", "redirectConfig": { "protocol": "HTTPS", "port": "443", "statusCode": "HTTP_301"}}', 590 | }); 591 | 592 | ingressRules.splice(0, 0, { 593 | host: domainName, 594 | http: { 595 | paths: [ 596 | { 597 | path: '/', 598 | pathType: 'Prefix', 599 | backend: { 600 | service: { 601 | name: 'ssl-redirect', 602 | port: { 603 | name: 'use-annotation', 604 | }, 605 | }, 606 | }, 607 | }, 608 | { 609 | path: '/', 610 | pathType: 'Prefix', 611 | backend: { 612 | service: { 613 | name: `${nexus3ChartName}-sonatype-nexus`, 614 | port: { 615 | number: nexusPort, 616 | }, 617 | }, 618 | }, 619 | }, 620 | ], 621 | }, 622 | }); 623 | 624 | // install external dns 625 | const externalDNSNamespace = 'default'; 626 | const externalDNSServiceAccount = cluster.addServiceAccount('external-dns', { 627 | name: 'external-dns', 628 | namespace: externalDNSNamespace, 629 | }); 630 | 631 | const r53ListPolicy = new iam.PolicyStatement({ 632 | effect: iam.Effect.ALLOW, 633 | actions: [ 634 | 'route53:ListHostedZones', 635 | 'route53:ListResourceRecordSets', 636 | ], 637 | resources: ['*'], 638 | }); 639 | const r53UpdateRecordPolicy = new iam.PolicyStatement({ 640 | effect: iam.Effect.ALLOW, 641 | actions: [ 642 | 'route53:ChangeResourceRecordSets', 643 | ], 644 | 645 | resources: [hostedZone!.hostedZoneArn!], 646 | }); 647 | externalDNSServiceAccount.addToPrincipalPolicy(r53ListPolicy); 648 | externalDNSServiceAccount.addToPrincipalPolicy(r53UpdateRecordPolicy!); 649 | 650 | const externalDNSResources = yaml.safeLoadAll( 651 | request('GET', `${awsControllerBaseResourceBaseUrl}/examples/external-dns.yaml`) 652 | .getBody('utf-8').replace('external-dns-test.my-org.com', r53Domain ?? '') 653 | .replace('my-identifier', 'nexus3')) 654 | .filter((res: any) => { 655 | return res.kind != 'ServiceAccount'; 656 | }) 657 | .map((res: any) => { 658 | if (res.kind === 'Deployment') { 659 | res.spec.template.spec.containers[0].env = [ 660 | { 661 | name: 'AWS_REGION', 662 | value: cdk.Aws.REGION, 663 | }, 664 | ]; 665 | } 666 | return res; 667 | }); 668 | 669 | const externalDNS = cluster.addManifest('external-dns', ...externalDNSResources); 670 | externalDNS.node.addDependency(externalDNSServiceAccount); 671 | externalDNSResource = externalDNS; 672 | } 673 | 674 | const enableAutoConfigured: boolean = this.node.tryGetContext('enableAutoConfigured') || false; 675 | const nexus3ChartVersion = '5.4.0'; 676 | 677 | const nexus3PurgeFunc = new lambda_python.PythonFunction(this, 'Nexus3Purge', { 678 | description: 'Func purges the resources(such as pvc) left after deleting Nexus3 helm chart', 679 | entry: path.join(__dirname, '../lambda.d/nexus3-purge'), 680 | index: 'index.py', 681 | handler: 'handler', 682 | runtime: lambda.Runtime.PYTHON_3_9, 683 | environment: cluster.kubectlEnvironment, 684 | logRetention: logs.RetentionDays.ONE_MONTH, 685 | timeout: cdk.Duration.minutes(15), 686 | layers: [ 687 | new AwsCliLayer(this, 'AwsCliLayer'), 688 | new KubectlLayer(this, 'KubectlLayer'), 689 | ], 690 | vpc: vpc, 691 | securityGroups: cluster.kubectlSecurityGroup ? [cluster.kubectlSecurityGroup] : undefined, 692 | vpcSubnets: cluster.kubectlPrivateSubnets ? { subnets: cluster.kubectlPrivateSubnets } : undefined, 693 | }); 694 | nexus3PurgeFunc.role!.addToPrincipalPolicy(new iam.PolicyStatement({ 695 | actions: ['eks:DescribeCluster'], 696 | resources: [cluster.clusterArn], 697 | })); 698 | // allow this handler to assume the kubectl role 699 | cluster.kubectlRole!.grant(nexus3PurgeFunc.role!, 'sts:AssumeRole'); 700 | 701 | const nexus3PurgeCR = new cdk.CustomResource(this, 'Nexus3PurgeCR', { 702 | serviceToken: nexus3PurgeFunc.functionArn, 703 | resourceType: 'Custom::Nexus3-Purge', 704 | properties: { 705 | ClusterName: cluster.clusterName, 706 | RoleArn: cluster.kubectlRole!.roleArn, 707 | ObjectType: 'ingress', 708 | ObjectName: `${nexus3ChartName}-sonatype-nexus`, 709 | ObjectNamespace: nexus3Namespace, 710 | JsonPath: '.status.loadBalancer.ingress[0].hostname', 711 | TimeoutSeconds: cdk.Duration.minutes(6).toSeconds(), 712 | Release: nexus3ChartName, 713 | }, 714 | }); 715 | nexus3PurgeCR.node.addDependency(efsPV); 716 | nexus3PurgeCR.node.addDependency(awsLoadBalancerControllerChart); 717 | 718 | let nexus3ChartProperties: { [key: string]: any } = { 719 | statefulset: { 720 | enabled: true, 721 | }, 722 | initAdminPassword: { 723 | enabled: true, 724 | password: adminInitPassword.valueAsString, 725 | }, 726 | nexus: { 727 | imageName: partitionMapping.findInMap(cdk.Aws.PARTITION, 'nexus'), 728 | resources: { 729 | requests: { 730 | memory: '4800Mi', 731 | }, 732 | }, 733 | livenessProbe: { 734 | path: healthcheckPath, 735 | }, 736 | }, 737 | nexusProxy: { 738 | enabled: false, 739 | }, 740 | persistence: { 741 | enabled: true, 742 | storageClass: efsClass, 743 | accessMode: 'ReadWriteMany', 744 | }, 745 | nexusBackup: { 746 | enabled: false, 747 | persistence: { 748 | enabled: false, 749 | }, 750 | }, 751 | nexusCloudiam: { 752 | enabled: false, 753 | persistence: { 754 | enabled: false, 755 | }, 756 | }, 757 | ingress: { 758 | enabled: true, 759 | path: '/*', 760 | annotations: albOptions, 761 | tls: { 762 | enabled: false, 763 | }, 764 | rules: ingressRules, 765 | }, 766 | serviceAccount: { 767 | create: false, 768 | // uncomment below line when using IRSA for nexus 769 | // name: nexusServiceAccount.serviceAccountName, 770 | }, 771 | }; 772 | if (enableAutoConfigured) { 773 | // enalbe script feature of nexus3 774 | nexus3ChartProperties = { 775 | ...nexus3ChartProperties, 776 | config: { 777 | enabled: true, 778 | data: { 779 | 'nexus.properties': 'nexus.scripts.allowCreation=true', 780 | }, 781 | }, 782 | deployment: { 783 | additionalVolumeMounts: [ 784 | { 785 | mountPath: '/nexus-data/etc/nexus.properties', 786 | subPath: 'nexus.properties', 787 | name: 'sonatype-nexus-conf', 788 | }, 789 | ], 790 | }, 791 | }; 792 | } 793 | const nexus3Chart = cluster.addHelmChart('Nexus3', { 794 | chart: 'sonatype-nexus', 795 | repository: partitionMapping.findInMap(cdk.Aws.PARTITION, 'nexusHelmChartRepo'), 796 | namespace: nexus3Namespace, 797 | release: nexus3ChartName, 798 | version: nexus3ChartVersion, 799 | wait: true, 800 | timeout: cdk.Duration.minutes(15), 801 | values: nexus3ChartProperties, 802 | }); 803 | nexus3Chart.node.addDependency(nexusServiceAccount); 804 | nexus3Chart.node.addDependency(nexus3PurgeCR); 805 | if (certificate) { 806 | nexus3PurgeCR.node.addDependency(certificate); 807 | nexus3Chart.node.addDependency(certificate); 808 | nexus3Chart.node.addDependency(externalDNSResource!); 809 | } 810 | const albAddress = new eks.KubernetesObjectValue(this, 'Nexus3ALBAddress', { 811 | cluster, 812 | objectType: 'ingress', 813 | objectNamespace: nexus3Namespace, 814 | objectName: `${nexus3ChartName}-sonatype-nexus`, 815 | jsonPath: '.status.loadBalancer.ingress[0].hostname', 816 | }); 817 | albAddress.node.addDependency(nexus3Chart); 818 | 819 | if (enableAutoConfigured) { 820 | const nexusEndpointHostname = `http://${albAddress.value}`; 821 | if (nexusEndpointHostname) { 822 | const autoConfigureFunc = new lambda_python.PythonFunction(this, 'Neuxs3AutoCofingure', { 823 | entry: path.join(__dirname, '../lambda.d/nexuspreconfigure'), 824 | index: 'index.py', 825 | handler: 'handler', 826 | runtime: lambda.Runtime.PYTHON_3_9, 827 | logRetention: logs.RetentionDays.ONE_MONTH, 828 | timeout: cdk.Duration.minutes(5), 829 | vpc: vpc, 830 | }); 831 | 832 | const nexus3AutoConfigureCR = new cdk.CustomResource(this, 'Neuxs3AutoCofingureCustomResource', { 833 | serviceToken: autoConfigureFunc.functionArn, 834 | resourceType: 'Custom::Nexus3-AutoConfigure', 835 | properties: { 836 | Username: 'admin', 837 | Password: adminInitPassword.valueAsString, 838 | Endpoint: nexusEndpointHostname, 839 | S3BucketName: nexusBlobBucket.bucketName, 840 | }, 841 | }); 842 | nexus3AutoConfigureCR.node.addDependency(nexus3Chart); 843 | 844 | const addCondition = (): void => { 845 | if (eksVersion) { 846 | const eksV119 = new cdk.CfnCondition(this, 'EKSV1.19', { 847 | expression: cdk.Fn.conditionNot(cdk.Fn.conditionEquals('1.19', eksVersion.valueAsString)), 848 | }); 849 | autoConfigureFunc.node.children.forEach(r => { 850 | if (r instanceof cdk.CfnResource) { 851 | (r as cdk.CfnResource).cfnOptions.condition = eksV119; 852 | } else { 853 | r.node.children.forEach(r1 => { 854 | if (r1 instanceof cdk.CfnResource) { 855 | (r1 as cdk.CfnResource).cfnOptions.condition = eksV119; 856 | } 857 | }); 858 | } 859 | 860 | }); 861 | nexus3AutoConfigureCR.node.children.forEach(r => { 862 | (r as cdk.CfnResource).cfnOptions.condition = eksV119; 863 | }); 864 | } 865 | }; 866 | addCondition(); 867 | } 868 | } 869 | 870 | cdk.Aspects.of(cdk.Stack.of(cluster)).add({ 871 | visit: (node: IConstruct) => { 872 | if (node instanceof lambda.CfnFunction) { 873 | node.addPropertyOverride('Environment.Variables.AWS_STS_REGIONAL_ENDPOINTS', 'regional'); 874 | } 875 | }, 876 | }); 877 | 878 | // the hardcode id is copied from https://github.com/aws/aws-cdk/blob/099b5840cc5b45bad987b7e797e6009d6383a3a7/packages/%40aws-cdk/aws-logs/lib/log-retention.ts#L119 879 | (cdk.Stack.of(this).node.findChild('LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8a') 880 | .node.findChild('ServiceRole').node.findChild('DefaultPolicy').node 881 | .findChild('Resource') as cdk.CfnResource).addMetadata('cfn_nag', { 882 | rules_to_suppress: [ 883 | { 884 | id: 'W12', 885 | reason: 'wildcard in policy is built by CDK', 886 | }, 887 | ], 888 | }); 889 | 890 | new cdk.CfnOutput(this, 'nexus-oss-s3-bucket-blobstore', { 891 | value: `${nexusBlobBucket.bucketName}`, 892 | description: 'S3 Bucket created for Nexus OSS Blobstore', 893 | }); 894 | new cdk.CfnOutput(this, 'nexus-oss-alb-domain', { 895 | value: `${albAddress.value}`, 896 | description: 'load balancer domain of Nexus OSS', 897 | }); 898 | 899 | this.templateOptions.description = `(SO8020) - Sonatype Nexus Repository OSS on AWS. Template version ${pjson.version}`; 900 | } 901 | 902 | /** 903 | * The info is retrieved from https://github.com/kubernetes-sigs/aws-load-balancer-controller/releases 904 | */ 905 | getAwsLoadBalancerControllerRepo() { 906 | const albImageMapping = new cdk.CfnMapping(this, 'ALBImageMapping', { 907 | mapping: { 908 | 'me-south-1': { 909 | 2: '558608220178', 910 | }, 911 | 'eu-south-1': { 912 | 2: '590381155156', 913 | }, 914 | 'ap-northeast-1': { 915 | 2: '602401143452', 916 | }, 917 | 'ap-northeast-2': { 918 | 2: '602401143452', 919 | }, 920 | 'ap-south-1': { 921 | 2: '602401143452', 922 | }, 923 | 'ap-southeast-1': { 924 | 2: '602401143452', 925 | }, 926 | 'ap-southeast-2': { 927 | 2: '602401143452', 928 | }, 929 | 'ca-central-1': { 930 | 2: '602401143452', 931 | }, 932 | 'eu-central-1': { 933 | 2: '602401143452', 934 | }, 935 | 'eu-north-1': { 936 | 2: '602401143452', 937 | }, 938 | 'eu-west-1': { 939 | 2: '602401143452', 940 | }, 941 | 'eu-west-2': { 942 | 2: '602401143452', 943 | }, 944 | 'eu-west-3': { 945 | 2: '602401143452', 946 | }, 947 | 'sa-east-1': { 948 | 2: '602401143452', 949 | }, 950 | 'us-east-1': { 951 | 2: '602401143452', 952 | }, 953 | 'us-east-2': { 954 | 2: '602401143452', 955 | }, 956 | 'us-west-1': { 957 | 2: '602401143452', 958 | }, 959 | 'us-west-2': { 960 | 2: '602401143452', 961 | }, 962 | 'ap-east-1': { 963 | 2: '800184023465', 964 | }, 965 | 'af-south-1': { 966 | 2: '877085696533', 967 | }, 968 | 'cn-north-1': { 969 | 2: '918309763551', 970 | }, 971 | 'cn-northwest-1': { 972 | 2: '961992271922', 973 | }, 974 | }, 975 | }); 976 | return `${albImageMapping.findInMap(cdk.Aws.REGION, '2')}.dkr.ecr.${cdk.Aws.REGION}.${cdk.Aws.URL_SUFFIX}/amazon/aws-load-balancer-controller`; 977 | } 978 | 979 | private azOfSubnets(subnets: ec2.ISubnet[]): number { 980 | return new Set(subnets.map(subnet => subnet.availabilityZone)).size; 981 | } 982 | } 983 | -------------------------------------------------------------------------------- /src/sonatype-nexus3.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import * as cdk from 'aws-cdk-lib'; 3 | import { SonatypeNexus3Stack } from './lib/sonatype-nexus3-stack'; 4 | 5 | const app = new cdk.App(); 6 | const vpcId = app.node.tryGetContext('vpcId'); 7 | const env = vpcId ? { 8 | account: process.env.CDK_DEFAULT_ACCOUNT, 9 | region: process.env.CDK_DEFAULT_REGION, 10 | } : undefined; 11 | 12 | new SonatypeNexus3Stack(app, 'SonatypeNexus3OnEKS', { 13 | env: env, 14 | }); 15 | 16 | cdk.Tags.of(app).add('app', 'nexus3'); 17 | -------------------------------------------------------------------------------- /test/context-provider-mock.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema'; 3 | import * as cxapi from 'aws-cdk-lib/cx-api'; 4 | import { Construct } from 'constructs'; 5 | 6 | export interface MockVcpContextResponse { 7 | readonly vpcId: string; 8 | readonly vpcCidrBlock: string; 9 | readonly subnetGroups: cxapi.VpcSubnetGroup[]; 10 | } 11 | 12 | export function mockContextProviderWith( 13 | response: MockVcpContextResponse, 14 | paramValidator?: (options: cxschema.VpcContextQuery) => void) { 15 | const previous = cdk.ContextProvider.getValue; 16 | cdk.ContextProvider.getValue = (_scope: Construct, options: cdk.GetContextValueOptions) => { 17 | if (options.provider === cxschema.ContextProvider.VPC_PROVIDER) { 18 | if (paramValidator) { 19 | paramValidator(options.props as any); 20 | } 21 | 22 | return { 23 | value: { 24 | availabilityZones: [], 25 | isolatedSubnetIds: undefined, 26 | isolatedSubnetNames: undefined, 27 | isolatedSubnetRouteTableIds: undefined, 28 | privateSubnetIds: undefined, 29 | privateSubnetNames: undefined, 30 | privateSubnetRouteTableIds: undefined, 31 | publicSubnetIds: undefined, 32 | publicSubnetNames: undefined, 33 | publicSubnetRouteTableIds: undefined, 34 | ...response, 35 | } as cxapi.VpcContextResponse, 36 | }; 37 | } else if (options.provider === cxschema.ContextProvider.HOSTED_ZONE_PROVIDER) { 38 | return { 39 | value: { 40 | Id: '12345678', 41 | Name: 'example.com', 42 | }, 43 | }; 44 | } else { 45 | // unreachable 46 | expect(false); 47 | return { 48 | value: {}, 49 | }; 50 | } 51 | }; 52 | return previous; 53 | } 54 | 55 | export function restoreContextProvider(previous: (scope: Construct, options: cdk.GetContextValueOptions) => cdk.GetContextValueResult): void { 56 | cdk.ContextProvider.getValue = previous; 57 | } -------------------------------------------------------------------------------- /test/sonatype-nexus3.test.ts: -------------------------------------------------------------------------------- 1 | import * as cdk from 'aws-cdk-lib'; 2 | import { Template } from 'aws-cdk-lib/assertions'; 3 | import * as cxapi from 'aws-cdk-lib/cx-api'; 4 | import { Construct } from 'constructs'; 5 | import * as mock from './context-provider-mock'; 6 | import * as SonatypeNexus3 from '../src/lib/sonatype-nexus3-stack'; 7 | // @ts-ignore 8 | 9 | describe('Nexus OSS stack', () => { 10 | let app: cdk.App; 11 | let stack: cdk.Stack; 12 | const vpcId = 'vpc-123456'; 13 | let previous: (scope: Construct, options: cdk.GetContextValueOptions) => cdk.GetContextValueResult; 14 | 15 | const defaultContext = { 16 | enableR53HostedZone: true, 17 | }; 18 | 19 | beforeAll(() => { 20 | previous = mock.mockContextProviderWith({ 21 | vpcId, 22 | vpcCidrBlock: '10.58.0.0/16', 23 | subnetGroups: [ 24 | { 25 | name: 'ingress', 26 | type: cxapi.VpcSubnetGroupType.PUBLIC, 27 | subnets: [ 28 | { 29 | subnetId: 'subnet-000f2b20b0ebaef37', 30 | cidr: '10.58.0.0/22', 31 | availabilityZone: 'cn-northwest-1a', 32 | routeTableId: 'rtb-0f5312df5fe3ae508', 33 | }, 34 | { 35 | subnetId: 'subnet-0b2cce92f08506a9a', 36 | cidr: '10.58.4.0/22', 37 | availabilityZone: 'cn-northwest-1b', 38 | routeTableId: 'rtb-07e969fe93b6edd9a', 39 | }, 40 | { 41 | subnetId: 'subnet-0571b340c9f28375c', 42 | cidr: '10.58.8.0/22', 43 | availabilityZone: 'cn-northwest-1c', 44 | routeTableId: 'rtb-02ae139a60f628b5c', 45 | }, 46 | ], 47 | }, 48 | { 49 | name: 'private', 50 | type: cxapi.VpcSubnetGroupType.PRIVATE, 51 | subnets: [ 52 | { 53 | subnetId: 'subnet-0a6dab6bc063ea432', 54 | cidr: '10.58.32.0/19', 55 | availabilityZone: 'cn-northwest-1a', 56 | routeTableId: 'rtb-0be722c725fd0d29f', 57 | }, 58 | { 59 | subnetId: 'subnet-08dd359da55a6160b', 60 | cidr: '10.58.64.0/19', 61 | availabilityZone: 'cn-northwest-1b', 62 | routeTableId: 'rtb-0b13567ae92b08708', 63 | }, 64 | { 65 | subnetId: 'subnet-0d300d086b989eefc', 66 | cidr: '10.58.96.0/19', 67 | availabilityZone: 'cn-northwest-1c', 68 | routeTableId: 'rtb-08fe9e7932d86517e', 69 | }, 70 | ], 71 | }, 72 | ], 73 | }, _options => { 74 | }); 75 | }); 76 | 77 | afterAll(() => { 78 | mock.restoreContextProvider(previous); 79 | }); 80 | 81 | beforeEach(() => { 82 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, defaultContext)); 83 | }); 84 | 85 | test('Nexus Stack is created', () => { 86 | Template.fromStack(stack).hasResource('AWS::CloudFormation::Stack', {}); 87 | 88 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { 89 | Values: { 90 | 'Fn::Join': [ 91 | '', 92 | [ 93 | '{"statefulset":{"enabled":true},"initAdminPassword":{"enabled":true,"password":"', 94 | { 95 | Ref: 'NexusAdminInitPassword', 96 | }, 97 | '"},"nexus":{"imageName":"', 98 | { 99 | 'Fn::FindInMap': [ 100 | 'PartitionMapping', 101 | { 102 | Ref: 'AWS::Partition', 103 | }, 104 | 'nexus', 105 | ], 106 | }, 107 | '","resources":{"requests":{"memory":"4800Mi"}},"livenessProbe":{"path":"/"}},"nexusProxy":{"enabled":false},"persistence":{"enabled":true,"storageClass":"efs-sc","accessMode":"ReadWriteMany"},"nexusBackup":{"enabled":false,"persistence":{"enabled":false}},"nexusCloudiam":{"enabled":false,"persistence":{"enabled":false}},"ingress":{"enabled":true,"path":"/*","annotations":{"alb.ingress.kubernetes.io/backend-protocol":"HTTP","alb.ingress.kubernetes.io/healthcheck-path":"/","alb.ingress.kubernetes.io/healthcheck-port":8081,"alb.ingress.kubernetes.io/listen-ports":"[{\\"HTTP\\": 80}, {\\"HTTPS\\": 443}]","alb.ingress.kubernetes.io/scheme":"internet-facing","alb.ingress.kubernetes.io/inbound-cidrs":"0.0.0.0/0","alb.ingress.kubernetes.io/auth-type":"none","alb.ingress.kubernetes.io/target-type":"ip","kubernetes.io/ingress.class":"alb","alb.ingress.kubernetes.io/tags":"app=nexus3","alb.ingress.kubernetes.io/subnets":"', 108 | { 109 | Ref: 'NexusOSSVpcPublicSubnet1SubnetE287B3FC', 110 | }, 111 | ',', 112 | { 113 | Ref: 'NexusOSSVpcPublicSubnet2Subnet8D595BFF', 114 | }, 115 | '","alb.ingress.kubernetes.io/load-balancer-attributes":"access_logs.s3.enabled=true,access_logs.s3.bucket=', 116 | { 117 | Ref: 'LogBucketCC3B17E8', 118 | }, 119 | ',access_logs.s3.prefix=albAccessLog","alb.ingress.kubernetes.io/certificate-arn":"', 120 | { 121 | Ref: 'SSLCertificate2E93C565', 122 | }, 123 | '","alb.ingress.kubernetes.io/ssl-policy":"ELBSecurityPolicy-TLS-1-2-Ext-2018-06","alb.ingress.kubernetes.io/actions.ssl-redirect":"{\\"type\\": \\"redirect\\", \\"redirectConfig\\": { \\"protocol\\": \\"HTTPS\\", \\"port\\": \\"443\\", \\"statusCode\\": \\"HTTP_301\\"}}"},"tls":{"enabled":false},"rules":[{"host":"', 124 | { 125 | Ref: 'DomainName', 126 | }, 127 | '","http":{"paths":[{"path":"/","pathType":"Prefix","backend":{"service":{"name":"ssl-redirect","port":{"name":"use-annotation"}}}},{"path":"/","pathType":"Prefix","backend":{"service":{"name":"nexus3-sonatype-nexus","port":{"number":8081}}}}]}},{"http":{"paths":[{"path":"/","pathType":"Prefix","backend":{"service":{"name":"nexus3-sonatype-nexus","port":{"number":8081}}}}]}}]},"serviceAccount":{"create":false}}', 128 | ], 129 | ], 130 | }, 131 | Release: 'nexus3', 132 | Chart: 'sonatype-nexus', 133 | Version: '5.4.0', 134 | Namespace: 'default', 135 | Repository: { 136 | 'Fn::FindInMap': [ 137 | 'PartitionMapping', 138 | { 139 | Ref: 'AWS::Partition', 140 | }, 141 | 'nexusHelmChartRepo', 142 | ], 143 | }, 144 | Wait: true, 145 | Timeout: '900s', 146 | }); 147 | }); 148 | 149 | test('eks cluster is created with proper configuration', () => { 150 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-Cluster', { 151 | Config: { 152 | version: { 153 | Ref: 'KubernetesVersion', 154 | }, 155 | roleArn: { 156 | 'Fn::GetAtt': [ 157 | 'NexusClusterRole08D74DFC', 158 | 'Arn', 159 | ], 160 | }, 161 | resourcesVpcConfig: { 162 | subnetIds: [ 163 | { 164 | Ref: 'NexusOSSVpcPublicSubnet1SubnetE287B3FC', 165 | }, 166 | { 167 | Ref: 'NexusOSSVpcPublicSubnet2Subnet8D595BFF', 168 | }, 169 | { 170 | Ref: 'NexusOSSVpcPrivateSubnet1SubnetEFE22FB8', 171 | }, 172 | { 173 | Ref: 'NexusOSSVpcPrivateSubnet2Subnet8A12FC8A', 174 | }, 175 | ], 176 | securityGroupIds: [ 177 | { 178 | 'Fn::GetAtt': [ 179 | 'NexusClusterControlPlaneSecurityGroupBC441028', 180 | 'GroupId', 181 | ], 182 | }, 183 | ], 184 | endpointPublicAccess: false, 185 | endpointPrivateAccess: true, 186 | }, 187 | }, 188 | }); 189 | }); 190 | 191 | test('ssl certificate with R53 hosted zone when disabling R53 hosted zone', () => { 192 | const context = { 193 | enableR53HostedZone: false, 194 | }; 195 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context)); 196 | 197 | Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 0); 198 | }); 199 | 200 | test('ssl certificate with R53 hosted zone when enabling R53 hosted zone', () => { 201 | Template.fromStack(stack).hasResourceProperties('AWS::CertificateManager::Certificate', { 202 | DomainName: { 203 | Ref: 'DomainName', 204 | }, 205 | DomainValidationOptions: [ 206 | { 207 | DomainName: { 208 | Ref: 'DomainName', 209 | }, 210 | HostedZoneId: { 211 | Ref: 'R53HostedZoneId', 212 | }, 213 | }, 214 | ], 215 | ValidationMethod: 'DNS', 216 | }); 217 | }); 218 | 219 | test('Create Nexus Stack with new vpc and custom instanceType', () => { 220 | const context = { 221 | ...defaultContext, 222 | instanceType: 'm5.xlarge', 223 | }; 224 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context)); 225 | 226 | Template.fromStack(stack).hasResourceProperties('AWS::EC2::VPC', { 227 | CidrBlock: '10.0.0.0/16', 228 | }); 229 | 230 | Template.fromStack(stack).hasResourceProperties('AWS::EC2::LaunchTemplate', { 231 | LaunchTemplateData: { 232 | BlockDeviceMappings: [ 233 | { 234 | DeviceName: '/dev/xvda', 235 | Ebs: { 236 | Encrypted: true, 237 | VolumeSize: 30, 238 | }, 239 | }, 240 | ], 241 | Monitoring: { 242 | Enabled: true, 243 | }, 244 | }, 245 | }); 246 | Template.fromStack(stack).hasResourceProperties('AWS::EKS::Nodegroup', { 247 | InstanceTypes: ['m5.xlarge'], 248 | LaunchTemplate: { 249 | Id: { 250 | Ref: 'EKSManagedNodeTemplate423DB07D', 251 | }, 252 | }, 253 | }); 254 | }); 255 | 256 | test('Enable Nexus3 auto configuration', () => { 257 | const context = { 258 | ...defaultContext, 259 | enableAutoConfigured: true, 260 | }; 261 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context)); 262 | 263 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { 264 | Values: { 265 | 'Fn::Join': [ 266 | '', 267 | [ 268 | '{"clusterName":"', 269 | { 270 | Ref: 'NexusCluster2168A4B1', 271 | }, 272 | '","image":{"repository":"', 273 | { 274 | 'Fn::FindInMap': [ 275 | 'ALBImageMapping', 276 | { 277 | Ref: 'AWS::Region', 278 | }, 279 | '2', 280 | ], 281 | }, 282 | '.dkr.ecr.', 283 | { 284 | Ref: 'AWS::Region', 285 | }, 286 | '.', 287 | { 288 | Ref: 'AWS::URLSuffix', 289 | }, 290 | '/amazon/aws-load-balancer-controller"},"serviceAccount":{"create":false,"name":"aws-load-balancer-controller"},"enableShield":false,"enableWaf":false,"enableWafv2":false}', 291 | ], 292 | ], 293 | }, 294 | }); 295 | 296 | Template.fromStack(stack).hasResource('Custom::Nexus3-AutoConfigure', { 297 | Properties: { 298 | ServiceToken: { 299 | 'Fn::GetAtt': [ 300 | 'Neuxs3AutoCofingureE91D0A63', 301 | 'Arn', 302 | ], 303 | }, 304 | Username: 'admin', 305 | Password: { 306 | Ref: 'NexusAdminInitPassword', 307 | }, 308 | Endpoint: { 309 | 'Fn::Join': [ 310 | '', 311 | [ 312 | 'http://', 313 | { 314 | 'Fn::GetAtt': [ 315 | 'Nexus3ALBAddress17C0552F', 316 | 'Value', 317 | ], 318 | }, 319 | ], 320 | ], 321 | }, 322 | S3BucketName: { 323 | Ref: 'nexus3blobstore00DDADD3', 324 | }, 325 | }, 326 | DependsOn: [ 327 | 'NexusClusterchartNexus37BADE970', 328 | ], 329 | Condition: 'EKSV119', 330 | }); 331 | 332 | Template.fromStack(stack).hasResource('Custom::LogRetention', { 333 | Properties: { 334 | LogGroupName: { 335 | 'Fn::Join': [ 336 | '', 337 | [ 338 | '/aws/lambda/', 339 | { 340 | Ref: 'Neuxs3AutoCofingureE91D0A63', 341 | }, 342 | ], 343 | ], 344 | }, 345 | }, 346 | Condition: 'EKSV119', 347 | }); 348 | }); 349 | 350 | test('AWS load baalancer controller helm chart is created', () => { 351 | const context = { 352 | ...defaultContext, 353 | vpcId: 'default', 354 | }; 355 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context, { 356 | account: '123456789012', 357 | region: 'cn-north-1', 358 | })); 359 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { 360 | Release: 'aws-load-balancer-controller', 361 | Chart: 'aws-load-balancer-controller', 362 | Version: '1.4.4', 363 | Repository: { 364 | 'Fn::FindInMap': [ 365 | 'PartitionMapping', 366 | { 367 | Ref: 'AWS::Partition', 368 | }, 369 | 'albHelmChartRepo', 370 | ], 371 | }, 372 | }); 373 | }); 374 | 375 | test('External dns resource is created when r53Domain is specified.', () => { 376 | const context = { 377 | ...defaultContext, 378 | vpcId: 'default', 379 | }; 380 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context, { 381 | account: '123456789012', 382 | region: 'cn-north-1', 383 | })); 384 | 385 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-KubernetesResource', { 386 | Manifest: { 387 | 'Fn::Join': [ 388 | '', 389 | [ 390 | '[{"apiVersion":"v1","kind":"ServiceAccount","metadata":{"name":"external-dns","namespace":"default","labels":{"aws.cdk.eks/prune-c85512b0f3c9c03a9294d46c98f9f1357963ae570e":"","app.kubernetes.io/name":"external-dns"},"annotations":{"eks.amazonaws.com/role-arn":"', 391 | { 392 | 'Fn::GetAtt': [ 393 | 'NexusClusterexternaldnsRole25A6F41E', 394 | 'Arn', 395 | ], 396 | }, 397 | '"}}}]', 398 | ], 399 | ], 400 | }, 401 | }); 402 | }); 403 | 404 | test('custom purge lambda is expected', () => { 405 | // must use runtime py_39 for awscli 1.x support 406 | // must have env 'AWS_STS_REGIONAL_ENDPOINTS' for some regions, such as ap-east-1 407 | Template.fromStack(stack).hasResourceProperties('AWS::Lambda::Function', { 408 | Environment: { 409 | Variables: { 410 | AWS_STS_REGIONAL_ENDPOINTS: 'regional', 411 | }, 412 | }, 413 | Handler: 'index.handler', 414 | Layers: [ 415 | { 416 | Ref: 'AwsCliLayerF44AAF94', 417 | }, 418 | { 419 | Ref: 'KubectlLayer600207B5', 420 | }, 421 | ], 422 | Runtime: 'python3.9', 423 | }); 424 | }); 425 | 426 | test('correct dependencies for deleting stack', () => { 427 | // retain custom data after deleting stack 428 | Template.fromStack(stack).hasResource('AWS::EFS::FileSystem', { 429 | UpdateReplacePolicy: 'Retain', 430 | DeletionPolicy: 'Retain', 431 | }); 432 | 433 | // explicitly remove the sg of EFS for deleting the VPC 434 | Template.fromStack(stack).hasResource('AWS::EC2::SecurityGroup', { 435 | Properties: { 436 | SecurityGroupIngress: [ 437 | { 438 | CidrIp: { 439 | 'Fn::GetAtt': [ 440 | 'NexusOSSVpc94CE3B74', 441 | 'CidrBlock', 442 | ], 443 | }, 444 | Description: 'allow access efs from inside vpc', 445 | FromPort: 2049, 446 | IpProtocol: 'tcp', 447 | ToPort: 2049, 448 | }, 449 | ], 450 | }, 451 | UpdateReplacePolicy: 'Delete', 452 | DeletionPolicy: 'Delete', 453 | }); 454 | 455 | Template.fromStack(stack).hasResource('Custom::Nexus3-Purge', { 456 | Properties: { 457 | ServiceToken: { 458 | 'Fn::GetAtt': [ 459 | 'Nexus3PurgeE46D0DF0', 460 | 'Arn', 461 | ], 462 | }, 463 | ClusterName: { 464 | Ref: 'NexusCluster2168A4B1', 465 | }, 466 | RoleArn: { 467 | 'Fn::GetAtt': [ 468 | 'NexusClusterCreationRole5D1FBB93', 469 | 'Arn', 470 | ], 471 | }, 472 | ObjectType: 'ingress', 473 | ObjectName: 'nexus3-sonatype-nexus', 474 | ObjectNamespace: 'default', 475 | JsonPath: '.status.loadBalancer.ingress[0].hostname', 476 | TimeoutSeconds: 360, 477 | Release: 'nexus3', 478 | }, 479 | DependsOn: [ 480 | 'NexusClusterchartAWSLoadBalancerController06E2710B', 481 | 'NexusClustermanifestefspv19E0A105', 482 | 'SSLCertificate2E93C565', 483 | ], 484 | UpdateReplacePolicy: 'Delete', 485 | DeletionPolicy: 'Delete', 486 | }); 487 | 488 | Template.fromStack(stack).hasResource('Custom::AWSCDK-EKS-HelmChart', { 489 | Properties: { 490 | Release: 'nexus3', 491 | Chart: 'sonatype-nexus', 492 | }, 493 | DependsOn: [ 494 | 'Nexus3PurgeCR', 495 | 'NexusClusterKubectlReadyBarrier6571FFC0', 496 | 'NexusClustermanifestexternaldns8C93099A', 497 | 'NexusClustersonatypenexus3ConditionJsonBA718515', 498 | 'NexusClustersonatypenexus3manifestsonatypenexus3ServiceAccountResourceDA1D0F12', 499 | 'NexusClustersonatypenexus3RoleDefaultPolicy0CF1CA3B', 500 | 'NexusClustersonatypenexus3RoleFE3455FB', 501 | 'SSLCertificate2E93C565', 502 | ], 503 | }); 504 | }); 505 | 506 | test('the encryption configuration of storages.', () => { 507 | Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 508 | BucketEncryption: { 509 | ServerSideEncryptionConfiguration: [ 510 | { 511 | ServerSideEncryptionByDefault: { 512 | SSEAlgorithm: 'AES256', 513 | }, 514 | }, 515 | ], 516 | }, 517 | }); 518 | 519 | Template.fromStack(stack).hasResourceProperties('AWS::EFS::FileSystem', { 520 | Encrypted: true, 521 | }); 522 | }); 523 | 524 | test('bucket policy of log bucket, including, access log of ALB created by AWS load balancer controller, vpc flow logs.', () => { 525 | Template.fromStack(stack).hasResourceProperties('AWS::S3::BucketPolicy', { 526 | Bucket: { 527 | Ref: 'LogBucketCC3B17E8', 528 | }, 529 | PolicyDocument: { 530 | Statement: [ 531 | { 532 | Action: 's3:PutObject', 533 | Condition: { 534 | StringEquals: { 535 | 's3:x-amz-acl': 'bucket-owner-full-control', 536 | }, 537 | }, 538 | Effect: 'Allow', 539 | Principal: { 540 | Service: 'delivery.logs.amazonaws.com', 541 | }, 542 | Resource: { 543 | 'Fn::Join': [ 544 | '', 545 | [ 546 | { 547 | 'Fn::GetAtt': [ 548 | 'LogBucketCC3B17E8', 549 | 'Arn', 550 | ], 551 | }, 552 | '/vpcFlowLogs/AWSLogs/', 553 | { 554 | Ref: 'AWS::AccountId', 555 | }, 556 | '/*', 557 | ], 558 | ], 559 | }, 560 | Sid: 'AWSLogDeliveryWrite', 561 | }, 562 | { 563 | Action: [ 564 | 's3:GetBucketAcl', 565 | 's3:ListBucket', 566 | ], 567 | Effect: 'Allow', 568 | Principal: { 569 | Service: 'delivery.logs.amazonaws.com', 570 | }, 571 | Resource: { 572 | 'Fn::GetAtt': [ 573 | 'LogBucketCC3B17E8', 574 | 'Arn', 575 | ], 576 | }, 577 | Sid: 'AWSLogDeliveryCheck', 578 | }, 579 | { 580 | Action: [ 581 | 's3:PutObject', 582 | 's3:PutObjectLegalHold', 583 | 's3:PutObjectRetention', 584 | 's3:PutObjectTagging', 585 | 's3:PutObjectVersionTagging', 586 | 's3:Abort*', 587 | ], 588 | Effect: 'Allow', 589 | Principal: { 590 | AWS: { 591 | 'Fn::Join': [ 592 | '', 593 | [ 594 | 'arn:', 595 | { 596 | Ref: 'AWS::Partition', 597 | }, 598 | ':iam::', 599 | { 600 | 'Fn::FindInMap': [ 601 | 'ALBServiceAccountMapping', 602 | { 603 | Ref: 'AWS::Region', 604 | }, 605 | 'account', 606 | ], 607 | }, 608 | ':root', 609 | ], 610 | ], 611 | }, 612 | }, 613 | Resource: { 614 | 'Fn::Join': [ 615 | '', 616 | [ 617 | { 618 | 'Fn::GetAtt': [ 619 | 'LogBucketCC3B17E8', 620 | 'Arn', 621 | ], 622 | }, 623 | '/albAccessLog/AWSLogs/', 624 | { 625 | Ref: 'AWS::AccountId', 626 | }, 627 | '/*', 628 | ], 629 | ], 630 | }, 631 | }, 632 | ], 633 | }, 634 | }); 635 | }); 636 | 637 | test('deploy alb as interal.', () => { 638 | const context = { 639 | ...defaultContext, 640 | internalALB: true, 641 | }; 642 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context)); 643 | 644 | Template.fromStack(stack).resourceCountIs('AWS::CertificateManager::Certificate', 0); 645 | 646 | Template.fromStack(stack).hasResourceProperties('Custom::AWSCDK-EKS-HelmChart', { 647 | Release: 'nexus3', 648 | Values: { 649 | 'Fn::Join': [ 650 | '', 651 | [ 652 | '{"statefulset":{"enabled":true},"initAdminPassword":{"enabled":true,"password":"', 653 | { 654 | Ref: 'NexusAdminInitPassword', 655 | }, 656 | '"},"nexus":{"imageName":"', 657 | { 658 | 'Fn::FindInMap': [ 659 | 'PartitionMapping', 660 | { 661 | Ref: 'AWS::Partition', 662 | }, 663 | 'nexus', 664 | ], 665 | }, 666 | '","resources":{"requests":{"memory":"4800Mi"}},"livenessProbe":{"path":"/"}},"nexusProxy":{"enabled":false},"persistence":{"enabled":true,"storageClass":"efs-sc","accessMode":"ReadWriteMany"},"nexusBackup":{"enabled":false,"persistence":{"enabled":false}},"nexusCloudiam":{"enabled":false,"persistence":{"enabled":false}},"ingress":{"enabled":true,"path":"/*","annotations":{"alb.ingress.kubernetes.io/backend-protocol":"HTTP","alb.ingress.kubernetes.io/healthcheck-path":"/","alb.ingress.kubernetes.io/healthcheck-port":8081,"alb.ingress.kubernetes.io/listen-ports":"[{\\"HTTP\\": 80}]","alb.ingress.kubernetes.io/scheme":"internal","alb.ingress.kubernetes.io/inbound-cidrs":"', 667 | { 668 | 'Fn::GetAtt': [ 669 | 'NexusOSSVpc94CE3B74', 670 | 'CidrBlock', 671 | ], 672 | }, 673 | '","alb.ingress.kubernetes.io/auth-type":"none","alb.ingress.kubernetes.io/target-type":"ip","kubernetes.io/ingress.class":"alb","alb.ingress.kubernetes.io/tags":"app=nexus3","alb.ingress.kubernetes.io/subnets":"', 674 | { 675 | Ref: 'NexusOSSVpcPublicSubnet1SubnetE287B3FC', 676 | }, 677 | ',', 678 | { 679 | Ref: 'NexusOSSVpcPublicSubnet2Subnet8D595BFF', 680 | }, 681 | '","alb.ingress.kubernetes.io/load-balancer-attributes":"access_logs.s3.enabled=true,access_logs.s3.bucket=', 682 | { 683 | Ref: 'LogBucketCC3B17E8', 684 | }, 685 | ',access_logs.s3.prefix=albAccessLog"},"tls":{"enabled":false},"rules":[{"http":{"paths":[{"path":"/","pathType":"Prefix","backend":{"service":{"name":"nexus3-sonatype-nexus","port":{"number":8081}}}}]}}]},"serviceAccount":{"create":false}}', 686 | ], 687 | ], 688 | }, 689 | }); 690 | }); 691 | 692 | test('deploy to existing eks cluster.', () => { 693 | const context = { 694 | ...defaultContext, 695 | importedEKS: true, 696 | vpcId: 'vpc-12345', 697 | eksClusterName: 'eks-cluster', 698 | eksKubectlRoleArn: 'arn:aws-cn:iam::123456789012:role/eks-kubectl-role', 699 | eksOpenIdConnectProviderArn: 'arn:aws-cn:iam::123456789012:oidc-provider/oidc.eks.cn-north-1.amazonaws.cn/id/123456789', 700 | nodeGroupRoleArn: 'arn:aws-cn:iam::123456789012:role/eksctl-cluster-nodegroup-ng-NodeInstanceRole-123456', 701 | }; 702 | ({ app, stack } = initializeStackWithContextsAndEnvs(app, stack, context, { 703 | account: '123456789012', 704 | region: 'cn-north-1', 705 | })); 706 | 707 | Template.fromStack(stack).resourceCountIs('Custom::AWSCDK-EKS-Cluster', 0); 708 | Template.fromStack(stack).resourceCountIs('AWS::EKS::Nodegroup', 0); 709 | }); 710 | 711 | }); 712 | 713 | function initializeStackWithContextsAndEnvs(app: cdk.App, stack: cdk.Stack, 714 | context: {} | undefined, env?: {} | undefined) { 715 | app = new cdk.App({ 716 | context, 717 | }); 718 | 719 | stack = new SonatypeNexus3.SonatypeNexus3Stack(app, 'NexusStack', { 720 | env: env, 721 | }); 722 | return { app, stack }; 723 | } 724 | -------------------------------------------------------------------------------- /tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "alwaysStrict": true, 4 | "declaration": true, 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "inlineSourceMap": true, 8 | "inlineSources": true, 9 | "lib": [ 10 | "es2019" 11 | ], 12 | "module": "CommonJS", 13 | "noEmitOnError": false, 14 | "noFallthroughCasesInSwitch": true, 15 | "noImplicitAny": true, 16 | "noImplicitReturns": true, 17 | "noImplicitThis": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "resolveJsonModule": true, 21 | "strict": true, 22 | "strictNullChecks": true, 23 | "strictPropertyInitialization": true, 24 | "stripInternal": true, 25 | "target": "ES2019" 26 | }, 27 | "include": [ 28 | ".projenrc.js", 29 | "src/**/*.ts", 30 | "test/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "node_modules" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": "src", 4 | "outDir": "lib", 5 | "alwaysStrict": true, 6 | "declaration": true, 7 | "esModuleInterop": true, 8 | "experimentalDecorators": true, 9 | "inlineSourceMap": true, 10 | "inlineSources": true, 11 | "lib": [ 12 | "es2019" 13 | ], 14 | "module": "CommonJS", 15 | "noEmitOnError": false, 16 | "noFallthroughCasesInSwitch": true, 17 | "noImplicitAny": true, 18 | "noImplicitReturns": true, 19 | "noImplicitThis": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "resolveJsonModule": true, 23 | "strict": true, 24 | "strictNullChecks": true, 25 | "strictPropertyInitialization": true, 26 | "stripInternal": true, 27 | "target": "ES2019" 28 | }, 29 | "include": [ 30 | "src/**/*.ts" 31 | ], 32 | "exclude": [ 33 | "cdk.out" 34 | ], 35 | "//": "~~ Generated by projen. To modify, edit .projenrc.js and run \"npx projen\"." 36 | } 37 | --------------------------------------------------------------------------------