├── .dockerignore ├── .github ├── pull_request_template.md ├── release.yml └── workflows │ ├── demo_deploy.yml │ ├── e2e_tests.yml │ ├── gh-pages.yml │ ├── production_release.yml │ └── pull_request.yml ├── .gitignore ├── .gitmodules ├── .log4brains.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPMENT.md ├── Dockerfile ├── Dockerfile.awslambda ├── LICENSE ├── NOTICE ├── PROJECT_GUIDELINES.md ├── README.md ├── api ├── PclusterApiHandler.py ├── __init__.py ├── exception │ ├── __init__.py │ ├── exceptions.py │ └── handlers.py ├── logging │ ├── __init__.py │ ├── http_info.py │ └── logger.py ├── pcm_globals.py ├── security │ ├── __init__.py │ ├── csrf │ │ ├── __init__.py │ │ ├── constants.py │ │ └── csrf.py │ ├── fingerprint.py │ └── headers.py ├── tests │ ├── conftest.py │ ├── exceptions │ │ ├── conftest.py │ │ └── test_exception_handlers.py │ ├── logging │ │ └── test_logger.py │ ├── security │ │ ├── csrf │ │ │ ├── conftest.py │ │ │ └── test_csrf.py │ │ ├── test_add_response_headers.py │ │ └── test_secret_generator.py │ ├── test_authenticate.py │ ├── test_get_app_config.py │ ├── test_get_identity.py │ ├── test_logout.py │ ├── test_pcluster_api_handler.py │ ├── test_push_log.py │ ├── test_revoke_cognito_refresh_token.py │ └── validation │ │ ├── test_api_custom_validators.py │ │ └── test_api_validation.py ├── utils.py └── validation │ ├── __init__.py │ ├── schemas.py │ └── validators.py ├── app.py ├── awslambda ├── __init__.py ├── entrypoint.py └── serverless_wsgi.py ├── decisions ├── 20220708-use-log4brains-to-manage-the-adrs.md ├── 20220708-use-michael-nygard-format-for-adr.md ├── 20220713-use-github-actions-for-pipelines.md ├── 20221025-public-documentation-release.md ├── 20221027-export-pcluster-manager-logs-from-cloudwatch.md ├── 20221027-support-multiple-versions-of-pc-api.md ├── 20221107-adopt-github-labels-to-allow-automating-release-changelog-creation.md ├── 20221129-adopt-releaselabels-to-automate-changelog-generation.md ├── 20221206-frontend-log-instrumentation.md ├── 20230125-pcui-versioning-strategy.md ├── README.md ├── index.md └── template.md ├── docs ├── .gitignore ├── README.md ├── archetypes │ └── default.md ├── config.toml ├── content │ ├── 01-getting-started │ │ ├── 01-aws-console-login.md │ │ ├── 02-pcmanager-install.md │ │ ├── 03-pcmanager-connect.md │ │ ├── 04-summary.md │ │ └── _index.md │ ├── 02-tutorials │ │ ├── 01-setup-mfa.md │ │ ├── 02-slurm-accounting.md │ │ ├── 03-memory-scheduling.md │ │ ├── 04-cost-tracking.md │ │ ├── 05-cloud9.md │ │ ├── 06-downloading.md │ │ ├── 07-setup-iam.md │ │ ├── 08-custom-domain.md │ │ └── _index.md │ └── _index.md ├── layouts │ └── partials │ │ └── logo.html └── static │ ├── 01-getting-started │ ├── aws-console.png │ ├── cfn-outputs.png │ ├── create.png │ ├── login.png │ ├── main-page.png │ ├── pcm-email.png │ ├── pcmanager-creds.png │ ├── pcmanager-deploy.png │ ├── pcmanager-deployed.png │ ├── pcmanager-first-page.png │ ├── pcmanager-install.png │ ├── pcmanager-stack.png │ ├── pcmanager-url.png │ ├── signup.png │ ├── slurm-accounting-cfn-outputs.png │ ├── slurm-accounting-cfn-properties.png │ ├── slurm-accounting-cluster-properties.png │ ├── slurm-accounting-headnode-additional.png │ ├── slurm-accounting-headnode-subnet.png │ ├── slurm-accounting-headnode-virtual-console.png │ ├── slurm-accounting-job-details.png │ ├── slurm-accounting-job-list.png │ ├── slurm-accounting-policy.png │ ├── slurm-accounting-submit-job-dialog.png │ └── slurm-accounting-submit-job.png │ ├── 02-tutorials │ ├── 01-setup-mfa │ │ ├── cognito-enable-mfa.png │ │ └── setup-authy.png │ ├── 02-slurm-accounting │ │ ├── README.md │ │ ├── additional-sg.png │ │ ├── architecture.png │ │ ├── attach-admin.png │ │ ├── attach-policies.jpeg │ │ ├── attach-policies.png │ │ ├── cfn-outputs.jpeg │ │ ├── cfn-outputs.png │ │ ├── cfn-properties.png │ │ ├── cfn-templates │ │ │ └── accounting-cluster-template.yaml │ │ ├── cluster-properties.png │ │ ├── headnode-additional.png │ │ ├── headnode-additionals.png │ │ ├── headnode-virtual-console.png │ │ ├── job-details.png │ │ ├── job-list.png │ │ ├── lambda-permissions.jpeg │ │ ├── pcluster │ │ │ ├── cluster-config.yaml.template │ │ │ ├── generate-config.sh │ │ │ ├── post-install.sh │ │ │ └── sacct │ │ │ │ ├── slurm_sacct.conf │ │ │ │ └── slurmdbd.conf │ │ ├── submit-job-dialog.png │ │ └── submit-job.png │ ├── 04-cost-tracking │ │ ├── cost-explorer-project.png │ │ ├── cost-tags-computefleet.png │ │ ├── cost-tags-headnode.png │ │ └── create-budget.png │ ├── 05-cloud9 │ │ ├── cloud9-1.png │ │ ├── cloud9-2.png │ │ ├── cloud9-3.png │ │ ├── cloud9-4.png │ │ ├── cloud9-5.png │ │ └── cloud9.png │ ├── 06-downloading │ │ └── downloader.png │ ├── 07-setup-iam │ │ ├── attach-policies.png │ │ └── lambda-permissions.jpeg │ ├── 08-custom-domain │ │ ├── api-mappings.png │ │ ├── certificate.png │ │ ├── cognito-custom-domain.png │ │ ├── create-domain.png │ │ ├── hosted-ui.png │ │ └── site-url.png │ ├── create.png │ └── memory-scheduling │ │ ├── HeadNode-Setup.png │ │ └── memory.png │ ├── architecture.png │ ├── css │ └── theme-mine.css │ └── parallelcluster-manager.svg ├── e2e ├── .gitignore ├── configs │ ├── environment.ts │ └── login.ts ├── fixtures │ └── wizard.template.yaml ├── package-lock.json ├── package.json ├── playwright.config.ts ├── specs │ ├── noMatch.spec.ts │ ├── wizard.spec.ts │ └── wizard.template.spec.ts └── test-utils │ └── login.ts ├── frontend ├── .dockerignore ├── .eslintrc.json ├── .husky │ ├── commit-msg │ ├── pre-commit │ └── prepare-commit-msg ├── .nvmrc ├── .prettierrc.json ├── Dockerfile ├── jest.config.js ├── locales │ └── en │ │ └── strings.json ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── public │ ├── favicon.ico │ ├── img │ │ ├── 3P-Logos.NOTICE │ │ ├── ebs.svg │ │ ├── ec2.svg │ │ ├── efs.svg │ │ ├── error_pages_illustration.svg │ │ ├── fsx.svg │ │ ├── od.svg │ │ ├── od_1.svg │ │ ├── pcluster.svg │ │ └── queue.svg │ ├── manifest.json │ ├── robots.txt │ └── third-party │ │ └── ace-1.4.13 │ │ ├── ace.min.js │ │ ├── ext-language_tools.js │ │ ├── mode-yaml.js │ │ └── theme-dawn.js ├── resources │ └── attributions │ │ └── npm-python-attributions.txt ├── scripts │ └── git-secrets-command.sh ├── src │ ├── __tests__ │ │ ├── CreateCluster.test.ts │ │ ├── DescribeCluster.test.ts │ │ ├── console.test.tsx │ │ ├── storagCreationValidation.test.tsx │ │ └── util.test.tsx │ ├── app-config │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── index.ts │ │ └── types.ts │ ├── auth │ │ ├── __tests__ │ │ │ └── handleNotAuthorizedErrors.test.ts │ │ ├── constants.ts │ │ └── handleNotAuthorizedErrors.ts │ ├── components │ │ ├── ConfigView.tsx │ │ ├── DeleteDialog.tsx │ │ ├── EmptyState.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FileChooser.tsx │ │ ├── InfoLink.tsx │ │ ├── InputErrors.tsx │ │ ├── Loading.tsx │ │ ├── NoMatch.tsx │ │ ├── SideBar.tsx │ │ ├── Status.tsx │ │ ├── TopBar.tsx │ │ ├── ValidationErrors.tsx │ │ ├── ValueWithLabel.tsx │ │ ├── __tests__ │ │ │ ├── ErrorBoundary.test.tsx │ │ │ ├── TopBar.test.tsx │ │ │ └── useClusterPoll.test.tsx │ │ ├── date │ │ │ ├── AbsoluteTimestamp.tsx │ │ │ ├── DateView.tsx │ │ │ └── __tests__ │ │ │ │ └── DateView.test.tsx │ │ ├── help-panel │ │ │ ├── DefaultHelpPanel.tsx │ │ │ ├── HelpPanel.tsx │ │ │ └── TitleDescriptionHelpPanel.tsx │ │ └── useClusterPoll.tsx │ ├── feature-flags │ │ ├── __tests__ │ │ │ ├── FeatureFlagsProvider.test.ts │ │ │ └── useFeatureFlag.test.ts │ │ ├── featureFlagsProvider.ts │ │ ├── types.ts │ │ └── useFeatureFlag.ts │ ├── http │ │ ├── __tests__ │ │ │ ├── csrf.test.ts │ │ │ ├── executeRequest.test.ts │ │ │ └── httpLogs.test.ts │ │ ├── csrf.ts │ │ ├── executeRequest.tsx │ │ └── httpLogs.ts │ ├── i18n-resources.d.ts │ ├── i18n │ │ └── index.ts │ ├── logger │ │ ├── ConsoleLogger.ts │ │ ├── ILogger.ts │ │ ├── LoggerProvider.tsx │ │ ├── RemoteLogger.ts │ │ └── __tests__ │ │ │ └── logger.test.ts │ ├── model.tsx │ ├── navigation │ │ ├── useLocationChangeLog.ts │ │ └── useWizardSectionChangeLog.ts │ ├── old-pages │ │ ├── Clusters │ │ │ ├── Accounting.tsx │ │ │ ├── Actions.tsx │ │ │ ├── Clusters.tsx │ │ │ ├── ConfigDialog.tsx │ │ │ ├── Details.tsx │ │ │ ├── Filesystems.tsx │ │ │ ├── Instances.tsx │ │ │ ├── Logs.tsx │ │ │ ├── Properties.tsx │ │ │ ├── Scheduling.tsx │ │ │ ├── StackEvents.tsx │ │ │ ├── StopDialog.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Clusters.test.tsx │ │ │ │ ├── Filesystems.test.ts │ │ │ │ └── util.test.ts │ │ │ └── util.tsx │ │ ├── Configure │ │ │ ├── Cluster.tsx │ │ │ ├── Components.tsx │ │ │ ├── Configure.tsx │ │ │ ├── Create.tsx │ │ │ ├── HeadNode.tsx │ │ │ ├── MultiUser.tsx │ │ │ ├── Queues │ │ │ │ ├── MultiInstanceComputeResource.tsx │ │ │ │ ├── Queues.test.tsx │ │ │ │ ├── Queues.tsx │ │ │ │ ├── SingleInstanceComputeResource.tsx │ │ │ │ ├── SlurmMemorySettings.tsx │ │ │ │ ├── SubnetMultiSelect.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── SubnetMultiSelect.test.tsx │ │ │ │ │ ├── hasMultipleInstanceTypes.test.tsx │ │ │ │ │ ├── mapComputeResources.test.ts │ │ │ │ │ └── multiAZ.test.tsx │ │ │ │ ├── queues.mapper.ts │ │ │ │ └── queues.types.ts │ │ │ ├── SlurmSettings │ │ │ │ ├── QueueUpdateStrategyForm.tsx │ │ │ │ ├── ScaledownIdleTimeForm.tsx │ │ │ │ ├── SlurmAccountingForm.tsx │ │ │ │ ├── SlurmSettings.tsx │ │ │ │ └── __tests__ │ │ │ │ │ ├── QueueUpdateStrategyForm.test.tsx │ │ │ │ │ ├── ScaledownIdleTimeForm.test.tsx │ │ │ │ │ └── slurmAccountingValidation.test.ts │ │ │ ├── Source.tsx │ │ │ ├── Storage.tsx │ │ │ ├── Storage.types.ts │ │ │ ├── __tests__ │ │ │ │ ├── dynamicFS.test.tsx │ │ │ │ ├── itemToOption.test.ts │ │ │ │ └── useWizardNavigation.test.tsx │ │ │ ├── storage.test.tsx │ │ │ ├── useWizardNavigation.ts │ │ │ └── util.tsx │ │ ├── CustomImages │ │ │ ├── CustomImageDetails.tsx │ │ │ ├── CustomImageStackEvents.tsx │ │ │ ├── CustomImages.tsx │ │ │ └── ImageBuildDialog.tsx │ │ ├── Layout.tsx │ │ ├── OfficialImages │ │ │ └── OfficialImages.tsx │ │ └── Users │ │ │ └── Users.tsx │ ├── pages │ │ ├── App.css │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ └── index.tsx │ ├── reportWebVitals.ts │ ├── store.ts │ ├── types │ │ ├── base.tsx │ │ ├── clusters.tsx │ │ ├── images.tsx │ │ ├── instances.tsx │ │ ├── jobs.tsx │ │ ├── logs.tsx │ │ ├── stackevents.tsx │ │ └── users.tsx │ └── util.ts └── tsconfig.json ├── infrastructure ├── README.md ├── SSMSessionProfile-cfn.yaml ├── bucket_configuration.sh ├── common.sh ├── environments │ ├── demo-cfn-update-args.yaml │ └── demo-variables.sh ├── github-env-setup-prod.yml ├── github-env-setup.yml ├── parallelcluster-ui-cognito.yaml ├── parallelcluster-ui.yaml ├── release_infrastructure.sh ├── slurm-accounting │ ├── accounting-cluster-template.yaml │ └── upload.sh ├── update-environment-infra.sh ├── update_md_files_links.sh └── upload.sh ├── pcluster_logo.png ├── pytest.ini ├── requirements.txt ├── resources ├── attributions │ └── docker-attributions.txt └── files │ └── sacct │ ├── slurm_accounting.rb │ ├── slurm_sacct.conf.erb │ ├── slurmdbd.conf.erb │ └── slurmdbd.service ├── scripts ├── README.md ├── build_and_release_image.sh ├── build_and_update_lambda.sh ├── cognito-tools │ ├── .gitignore │ ├── README.md │ ├── common.sh │ ├── export_cognito_users.sh │ └── import_cognito_users.sh ├── rollback_awslambda_image.sh ├── rollforward_awslambda_image.sh └── run_flask.sh └── uwsgi.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend 2 | frontend/* 3 | ./frontend/* 4 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | 4 | 5 | ## Changes 6 | 7 | 8 | 9 | ## Changelog entry 10 | 11 | 12 | 13 | ## How Has This Been Tested? 14 | 15 | 16 | 17 | ## References 18 | 19 | 20 | 21 | ## PR Quality Checklist 22 | 23 | - [ ] I added tests to new or existing code 24 | - [ ] I removed hardcoded strings and used our `i18n` solution instead (see [here](https://github.com/aws-samples/pcluster-manager/pull/175/commits/fdc6b77987c87a26f51dbc8da5d371d95ef80601)) 25 | - [ ] I made sure no sensitive info gets logged at any time in the codebase (see [here](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html)) (e.g. no user info or details, no stacktraces, etc.) 26 | - [ ] I checked that infrastructure/update_infrastructure.sh runs without any error 27 | - [ ] I checked that `npm run build` builds without any error 28 | - [ ] I checked that clusters are listed correctly 29 | - [ ] I checked that a new cluster can be created (config is produced and dry run passes) 30 | - [ ] I checked that login and logout work as expected 31 | 32 | In order to increase the likelihood of your contribution being accepted, please make sure you have read both the [Contributing Guidelines](../CONTRIBUTING.md) and the [Project Guidelines](../PROJECT_GUIDELINES.md) 33 | 34 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 35 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | 2 | changelog: 3 | exclude: 4 | labels: 5 | - DON'T MERGE 6 | - duplicate 7 | - invalid 8 | - question 9 | - wontfix 10 | 11 | categories: 12 | - title: Features 🎉 13 | labels: 14 | - release:feature 15 | 16 | - title: Changes 🏇 17 | labels: 18 | - release:improvement 19 | 20 | - title: Bugfixes 🐛 21 | labels: 22 | - release:bugfix 23 | 24 | - title: Deprecated ☢️ 25 | labels: 26 | - release:deprecated 27 | 28 | - title: Breaking Changes ☢️ 29 | labels: 30 | - release:breaking-change 31 | -------------------------------------------------------------------------------- /.github/workflows/e2e_tests.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Deploy to Demo"] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | id-token: write 11 | contents: read 12 | 13 | jobs: 14 | e2e-tests: 15 | timeout-minutes: 60 16 | runs-on: ubuntu-latest 17 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: 16 23 | - name: Configure AWS Credentials 24 | uses: aws-actions/configure-aws-credentials@v1 25 | with: 26 | aws-region: eu-west-1 27 | role-to-assume: ${{ secrets.ACTION_E2E_TESTS_ROLE }} 28 | 29 | - name: Retrieve test user email and password 30 | uses: aws-actions/aws-secretsmanager-get-secrets@v1 31 | with: 32 | secret-ids: | 33 | e2e/test1 34 | parse-json-secrets: true 35 | 36 | - name: Install dependencies 37 | run: npm ci 38 | working-directory: e2e 39 | - name: Install Playwright Browsers 40 | run: npx playwright install --with-deps 41 | working-directory: e2e 42 | - name: Run Playwright tests 43 | run: npm run e2e:test 44 | working-directory: e2e 45 | - uses: actions/upload-artifact@v3 46 | if: always() 47 | with: 48 | name: test-results 49 | path: e2e/test-results/ 50 | retention-days: 30 -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: Deploy public documentation 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | tags: '*' 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-20.04 11 | steps: 12 | - uses: actions/checkout@v2 13 | with: 14 | submodules: 'true' # Fetch Hugo themes (true OR recursive) 15 | fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod 16 | 17 | - name: Setup Hugo 18 | uses: peaceiris/actions-hugo@v2 19 | with: 20 | hugo-version: 'latest' 21 | # extended: true 22 | 23 | - name: Build 24 | run: | 25 | cd docs/ 26 | hugo --minify 27 | 28 | - name: Deploy 29 | uses: peaceiris/actions-gh-pages@v3 30 | if: github.ref == 'refs/heads/main' 31 | with: 32 | github_token: ${{ secrets.GITHUB_TOKEN }} 33 | publish_dir: ./docs/public 34 | -------------------------------------------------------------------------------- /.github/workflows/production_release.yml: -------------------------------------------------------------------------------- 1 | name: Release to production 2 | 3 | on: 4 | push: 5 | tags: '*' 6 | 7 | permissions: 8 | id-token: write 9 | contents: read 10 | 11 | jobs: 12 | frontend-tests: 13 | runs-on: ubuntu-20.04 14 | 15 | steps: 16 | - name: Checkout repo 17 | uses: actions/checkout@v3 18 | 19 | - uses: actions/setup-node@v3 20 | name: Setup Node version 21 | with: 22 | node-version-file: frontend/.nvmrc 23 | cache: 'npm' 24 | cache-dependency-path: frontend/package-lock.json 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | working-directory: ./frontend 29 | 30 | - name: Run linter 31 | run: npm run lint 32 | working-directory: ./frontend 33 | 34 | - name: Run type checks 35 | run: npm run ts-validate 36 | working-directory: ./frontend 37 | 38 | - name: Run frontend tests 39 | run: npm test 40 | working-directory: ./frontend 41 | 42 | backend-tests: 43 | runs-on: ubuntu-20.04 44 | 45 | steps: 46 | - name: Checkout repo 47 | uses: actions/checkout@v3 48 | 49 | - name: Setup Python version 50 | uses: actions/setup-python@v4 51 | with: 52 | python-version: '3.8' 53 | cache: 'pip' 54 | 55 | - name: Install python dependencies 56 | run: pip3 install -r requirements.txt 57 | 58 | - name: Run backend tests 59 | run: pytest 60 | 61 | release: 62 | runs-on: ubuntu-20.04 63 | 64 | needs: [frontend-tests, backend-tests] 65 | 66 | steps: 67 | - name: Checkout repo 68 | uses: actions/checkout@v3 69 | 70 | - name: Configure AWS Credentials 71 | uses: aws-actions/configure-aws-credentials@v1 72 | with: 73 | aws-region: us-east-1 74 | role-to-assume: ${{ secrets.ACTION_PRODUCTION_RELEASE_ROLE }} 75 | 76 | - name: Build and upload Docker image 77 | run: ./scripts/build_and_release_image.sh 78 | 79 | - name: Upload infrastructure files to S3 80 | run: ./infrastructure/release_infrastructure.sh -------------------------------------------------------------------------------- /.github/workflows/pull_request.yml: -------------------------------------------------------------------------------- 1 | 2 | name: Pull Request 3 | 4 | on: 5 | #By default, a workflow only runs when a pull_request event's activity type is opened, synchronize, or reopened. 6 | pull_request: 7 | 8 | jobs: 9 | frontend-tests: 10 | runs-on: ubuntu-20.04 11 | 12 | steps: 13 | - name: Checkout repo 14 | uses: actions/checkout@v3 15 | 16 | - uses: actions/setup-node@v3 17 | name: Setup Node version 18 | with: 19 | node-version-file: frontend/.nvmrc 20 | cache: 'npm' 21 | cache-dependency-path: frontend/package-lock.json 22 | 23 | - name: Install dependencies 24 | run: npm ci # https://docs.npmjs.com/cli/v8/commands/npm-ci 25 | working-directory: ./frontend 26 | 27 | - name: Run linter 28 | run: npm run lint 29 | working-directory: ./frontend 30 | 31 | - name: Run type checks 32 | run: npm run ts-validate 33 | working-directory: ./frontend 34 | 35 | - name: Run frontend tests 36 | run: npm test 37 | working-directory: ./frontend 38 | 39 | backend-tests: 40 | runs-on: ubuntu-20.04 41 | 42 | steps: 43 | - name: Checkout repo 44 | uses: actions/checkout@v3 45 | 46 | - name: Setup Python version 3.8 47 | uses: actions/setup-python@v4 48 | with: 49 | python-version: '3.8' 50 | cache: 'pip' 51 | 52 | - name: Install python dependencies 53 | run: if [ -f requirements.txt ]; then pip3 install -r requirements.txt ; fi 54 | 55 | - name: Run backend tests 56 | run: pytest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | frontend/node_modules 3 | frontend/.next 4 | frontend/.swc 5 | frontend/build 6 | infrastructure/cognitolambda/node_modules 7 | .DS_Store 8 | .hugo_build.lock 9 | public/* 10 | /frontend/tsconfig.tsbuildinfo 11 | .idea/ 12 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "docs/themes/hugo-theme-learn"] 2 | path = docs/themes/hugo-theme-learn 3 | url = https://github.com/matcornic/hugo-theme-learn.git 4 | -------------------------------------------------------------------------------- /.log4brains.yml: -------------------------------------------------------------------------------- 1 | project: 2 | name: ParallelCluster Manager (PCM) 3 | tz: Europe/Rome 4 | adrFolder: ./decisions 5 | packages: [] 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM tiangolo/uwsgi-nginx-flask:python3.8-alpine 2 | 3 | ENV PYTHONUNBUFFERED=1 4 | ENV STATIC_URL /static 5 | ENV STATIC_PATH /app/frontend/public/static 6 | 7 | WORKDIR /app 8 | 9 | COPY --from=frontend /app/build /app/frontend/public 10 | COPY ./requirements.txt /var/www/requirements.txt 11 | RUN pip install -r /var/www/requirements.txt 12 | ADD requirements.txt . 13 | RUN pip install -r requirements.txt 14 | 15 | RUN ls /app/frontend/public 16 | 17 | ADD api api 18 | ADD uwsgi.ini . 19 | ADD app.py . 20 | -------------------------------------------------------------------------------- /Dockerfile.awslambda: -------------------------------------------------------------------------------- 1 | FROM public.ecr.aws/lambda/python:3.8 2 | 3 | COPY --from=frontend-awslambda /app/build ${LAMBDA_TASK_ROOT}/frontend/public 4 | 5 | COPY resources/attributions/docker-attributions.txt license.txt 6 | COPY requirements.txt . 7 | RUN pip3 install -r requirements.txt --target "${LAMBDA_TASK_ROOT}" 8 | 9 | COPY app.py ${LAMBDA_TASK_ROOT} 10 | COPY api ${LAMBDA_TASK_ROOT}/api 11 | COPY awslambda ${LAMBDA_TASK_ROOT}/awslambda 12 | 13 | CMD ["awslambda.entrypoint.lambda_handler"] 14 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | -------------------------------------------------------------------------------- /PROJECT_GUIDELINES.md: -------------------------------------------------------------------------------- 1 | # Project Guidelines 2 | 3 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 4 | 5 | - You have added tests, even if just the happy path, when adding or refactoring code 6 | - You used types in your TypeScript code so that we can leverage static analysis to maintain quality 7 | - You have interationalized any user-facing text or copy, by using our `i18n` solution ([as shown here](https://github.com/aws-samples/pcluster-manager/pull/175/commits/fdc6b77987c87a26f51dbc8da5d371d95ef80601)) so that we can translate to other languages in the future 8 | - You have followed the PR Quality Checklist available in our [Pull Request Template](.github/pull_request_template.md) 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Warning 2 | 3 | ParallelCluster Manager has become an official feature of AWS ParallelCluster under the name [AWS ParallelCluster UI](https://github.com/aws/aws-parallelcluster-ui). Therefore, this repository is no longer supported. For the latest features and updates, we encourage customers to refer to the new [AWS ParallelCluster UI](https://github.com/aws/aws-parallelcluster-ui) repository and [documentation](https://docs.aws.amazon.com/parallelcluster/latest/ug/install-pcui-v3.html). 4 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/api/__init__.py -------------------------------------------------------------------------------- /api/exception/__init__.py: -------------------------------------------------------------------------------- 1 | from .exceptions import CSRFError 2 | from .handlers import ExceptionHandler 3 | -------------------------------------------------------------------------------- /api/exception/exceptions.py: -------------------------------------------------------------------------------- 1 | from werkzeug.exceptions import Forbidden 2 | 3 | 4 | class CSRFError(Forbidden): 5 | """Raise if the client sends invalid CSRF data with the request. 6 | Generates a 403 Forbidden response with the failure reason by default. 7 | Customize the response by registering a handler with 8 | :meth:`flask.Flask.errorhandler`. 9 | """ 10 | 11 | description = "CSRF validation failed." 12 | 13 | def __init__(self, description): 14 | self.description = description 15 | 16 | class RefreshTokenError(Exception): 17 | ERROR_FMT = 'Refresh token error: {description}' 18 | description = 'Refresh token flow failed' 19 | 20 | def __init__(self, description=None): 21 | if description: 22 | self.description = self.ERROR_FMT.format(description=description) 23 | 24 | def __str__(self): 25 | return self.description -------------------------------------------------------------------------------- /api/logging/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, request 2 | 3 | from api.logging.http_info import log_request_body_and_headers, log_response_body_and_headers 4 | 5 | VALID_LOG_LEVELS = {'debug', 'info', 'warning', 'error', 'critical'} 6 | 7 | def parse_log_entry(_logger, entry): 8 | """ 9 | Parse a log entry expected from PCM frontend and logs 10 | every single entry with the correct log level 11 | returns 12 | log level, 13 | message, 14 | extra dict (if present) 15 | """ 16 | level, message, extra = entry.get('level'), entry.get('message'), entry.get('extra') 17 | 18 | lowercase_level = level.lower() 19 | if lowercase_level not in VALID_LOG_LEVELS: 20 | raise ValueError('Level param must be a valid log level') 21 | 22 | return lowercase_level, message, extra 23 | 24 | 25 | def push_log_entry(_logger, level, message, extra): 26 | """ Logs a single log entry at the specified level """ 27 | logging_fun = getattr(_logger, level, None) 28 | logging_fun(message, extra=extra) 29 | 30 | 31 | class RequestResponseLogging: 32 | def __init__(self, logger, app: Flask = None, urls_deny_list=['/logs']): 33 | self.logger = logger 34 | self.urls_deny_list = urls_deny_list 35 | if app: 36 | self.init_app(app) 37 | 38 | def init_app(self, app): 39 | 40 | def log_request(): 41 | if request.path not in self.urls_deny_list: 42 | log_request_body_and_headers(self.logger, request) 43 | 44 | def log_response(response = None): 45 | if request.path not in self.urls_deny_list: 46 | log_response_body_and_headers(self.logger, response) 47 | return response 48 | 49 | app.before_request(log_request) 50 | app.after_request(log_response) 51 | -------------------------------------------------------------------------------- /api/logging/http_info.py: -------------------------------------------------------------------------------- 1 | from typing import Union 2 | 3 | from flask import Request, Response 4 | 5 | 6 | def log_request_body_and_headers(_logger, request: Request): 7 | details = __get_http_info(request) 8 | details['path'] = request.path 9 | if request.args: 10 | details['params'] = request.args 11 | 12 | if 'serverless.event' in request.environ: 13 | env = request.environ.get('serverless.event') 14 | if 'requestContext' in env and 'requestId' in env.get('requestContext'): 15 | details['apigw-request-id'] = env.get('requestContext').get('requestId') 16 | 17 | _logger.info(details) 18 | 19 | 20 | def log_response_body_and_headers(_logger, response: Response): 21 | details = __get_http_info(response) 22 | _logger.info(details) 23 | 24 | 25 | def __get_http_info(r: Union[Request,Response]) -> dict: 26 | headers = __filter_headers(r.headers) 27 | details = {'headers': headers} 28 | 29 | try: 30 | body = r.json 31 | if body: 32 | details['body'] = body 33 | except: 34 | pass 35 | 36 | return details 37 | 38 | 39 | def __filter_headers(headers: dict): 40 | """ utility function to remove sensitive information from request headers """ 41 | _headers = dict(headers) 42 | _headers.pop('Cookie', None) 43 | _headers.pop('X-CSRF-Token', None) 44 | return _headers 45 | -------------------------------------------------------------------------------- /api/logging/logger.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from sys import exc_info 3 | 4 | class DefaultLogger(object): 5 | def __init__(self, is_running_local): 6 | self.logger = logging.getLogger("pcluster-manager") 7 | if is_running_local: 8 | handler = logging.StreamHandler() 9 | handler.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(message)s")) 10 | self.logger.addHandler(handler) 11 | self.logger.setLevel(logging.DEBUG) 12 | else: 13 | self.logger.setLevel(logging.INFO) 14 | 15 | def _log_output(self, msg, extra): 16 | _extra = {} if extra is None else extra 17 | _extra["message"] = msg 18 | return _extra 19 | 20 | def debug(self, msg, extra=None): 21 | self.logger.debug(self._log_output(msg, extra)) 22 | 23 | def info(self, msg, extra=None): 24 | self.logger.info(self._log_output(msg, extra)) 25 | 26 | def warning(self, msg, extra=None): 27 | self.logger.warning(self._log_output(msg, extra)) 28 | 29 | def error(self, msg, extra=None): 30 | self.logger.error(self._log_output(msg, extra), exc_info=self.__is_exception_caught()) 31 | 32 | def __is_exception_caught(self): 33 | return exc_info() != (None, None, None) 34 | 35 | def critical(self, msg, extra=None): 36 | self.logger.critical(self._log_output(msg, extra), exc_info=True) 37 | -------------------------------------------------------------------------------- /api/pcm_globals.py: -------------------------------------------------------------------------------- 1 | from contextvars import ContextVar 2 | 3 | from flask import g, Response 4 | from flask.scaffold import Scaffold 5 | from werkzeug.local import LocalProxy 6 | 7 | from api.logging.logger import DefaultLogger 8 | 9 | _logger_ctxvar = ContextVar('pcm_logger') 10 | 11 | logger = LocalProxy(_logger_ctxvar) 12 | 13 | def set_auth_cookies_in_context(cookies: dict): 14 | g.auth_cookies = cookies 15 | 16 | def get_auth_cookies(): 17 | if 'auth_cookies' not in g: 18 | g.auth_cookies = {} 19 | 20 | return g.auth_cookies 21 | 22 | auth_cookies = LocalProxy(get_auth_cookies) 23 | 24 | def add_auth_cookies(response: Response): 25 | for name, value in auth_cookies.items(): 26 | response.set_cookie(name, value, httponly=True, secure=True, samesite='Lax') 27 | return response 28 | 29 | class PCMGlobals(object): 30 | def __init__(self, app: Scaffold = None, running_local=False): 31 | self.running_local = running_local 32 | if app is not None: 33 | self.init_app(app) 34 | 35 | def init_app(self, app: Scaffold): 36 | _logger = self.__create_logger() 37 | 38 | def set_global_logger_before_func(): 39 | _logger_ctxvar.set(_logger) 40 | 41 | app.before_request(set_global_logger_before_func) 42 | 43 | # required for setting auth cookies in case of a token refresh 44 | app.after_request(add_auth_cookies) 45 | 46 | 47 | def __create_logger(self): 48 | return DefaultLogger(self.running_local) 49 | -------------------------------------------------------------------------------- /api/security/__init__.py: -------------------------------------------------------------------------------- 1 | from api.security.headers import SecurityHeaders 2 | -------------------------------------------------------------------------------- /api/security/csrf/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask, Blueprint, current_app, jsonify, request 2 | 3 | from api.security.csrf.constants import CSRF_SECRET_KEY, SALT, CSRF_COOKIE_NAME 4 | from api.security.csrf.csrf import generate_csrf_token, set_csrf_cookie 5 | 6 | from api.security.fingerprint import IFingerprintGenerator 7 | 8 | csrf_blueprint = Blueprint('csrf', __name__) 9 | 10 | 11 | @csrf_blueprint.get('/csrf') 12 | def get_and_set_csrf_token(): 13 | csrf_secret_key = current_app.config.get(CSRF_SECRET_KEY) 14 | csrf_token = generate_csrf_token(csrf_secret_key, SALT) 15 | resp = jsonify(csrf_token=csrf_token) 16 | set_csrf_cookie(resp, csrf_token) 17 | return resp 18 | 19 | 20 | class CSRF(object): 21 | 22 | def __init__(self, app: Flask = None, fingerprint_generator=None): 23 | if app is not None and fingerprint_generator is not None: 24 | self.init_app(app, fingerprint_generator) 25 | 26 | def init_app(self, app, fingerprint_generator: IFingerprintGenerator): 27 | csrf_secret_key = fingerprint_generator.fingerprint() 28 | 29 | app.config['CSRF_SECRET_KEY'] = csrf_secret_key 30 | app.register_blueprint(csrf_blueprint) 31 | -------------------------------------------------------------------------------- /api/security/csrf/constants.py: -------------------------------------------------------------------------------- 1 | CSRF_SECRET_KEY = 'CSRF_SECRET_KEY' 2 | CSRF_TOKEN_HEADER = 'X-CSRF-Token' 3 | CSRF_COOKIE_NAME = 'csrf' 4 | SALT = 'pcm-csrf-salt' 5 | -------------------------------------------------------------------------------- /api/security/fingerprint.py: -------------------------------------------------------------------------------- 1 | from hashlib import pbkdf2_hmac 2 | from abc import ABC 3 | 4 | 5 | class IFingerprintGenerator(ABC): 6 | 7 | def fingerprint(self): 8 | pass 9 | 10 | SALT = 'cognito-fingerprint-salt'.encode() 11 | 12 | class CognitoFingerprintGenerator(IFingerprintGenerator): 13 | 14 | def __init__(self, client_id, client_secret, user_pool_id): 15 | self.client_id = client_id 16 | self.client_secret = client_secret 17 | self.user_pool_id = user_pool_id 18 | 19 | def fingerprint(self): 20 | to_encrypt = self.client_id + self.client_secret + self.user_pool_id 21 | return pbkdf2_hmac('sha256', to_encrypt.encode(), SALT, 500_000).hex() -------------------------------------------------------------------------------- /api/security/headers.py: -------------------------------------------------------------------------------- 1 | from flask import Response 2 | from flask.scaffold import Scaffold 3 | from flask_cors import CORS 4 | 5 | CORP_HEADERS = [ 6 | {'key': 'Cross-Origin-Resource-Policy', 'default': 'same-site'}, 7 | {'key': 'Cross-Origin-Embedder-Policy', 'default': 'require-corp'} 8 | ] 9 | 10 | SECURITY_HEADERS = [ 11 | {'key': 'X-Frame-Options', 'default': 'DENY'}, 12 | {'key': 'X-Content-Type-Options', 'default': 'nosniff'}, 13 | {'key': 'Referrer-Policy', 'default': 'strict-origin-when-cross-origin'}, 14 | {'key': 'Strict-Transport-Security', 'default': 'max-age=63072000; includeSubDomains; preload'}, 15 | {'key': 'Permissions-Policy', 'default': 'interest-cohort=()'}, 16 | {'key': 'X-XSS-Protection', 'default': '1; mode=block'} 17 | ] 18 | 19 | CSP_HEADER = { 20 | 'key': 'Content-Security-Policy', 21 | 'default': "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src data:; img-src 'self' data:; child-src blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'none';" 22 | } 23 | 24 | def add_security_headers(response: Response): 25 | for header in [*CORP_HEADERS, *SECURITY_HEADERS, CSP_HEADER]: 26 | response.headers.setdefault(**header) 27 | return response 28 | 29 | 30 | def add_security_headers_dev(response: Response): 31 | for header in SECURITY_HEADERS: 32 | response.headers.setdefault(**header) 33 | return response 34 | 35 | 36 | class SecurityHeaders(object): 37 | 38 | def __init__(self, app: Scaffold = None, running_local=False): 39 | self.running_local = running_local 40 | if app is not None: 41 | self.init_app(app) 42 | 43 | def init_app(self, app: Scaffold): 44 | if self.running_local: 45 | CORS(app) 46 | app.after_request(add_security_headers_dev) 47 | else: 48 | app.after_request(add_security_headers) 49 | -------------------------------------------------------------------------------- /api/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import api.security 4 | from app import run 5 | import app as _app 6 | 7 | @pytest.fixture(autouse=True) 8 | def mock_cognito_variables(mocker): 9 | mocker.patch.object(_app, 'CLIENT_ID', 'client-id') 10 | mocker.patch.object(_app, 'USER_POOL_ID', 'user-pool') 11 | mocker.patch.object(_app, 'CLIENT_SECRET', 'client-secret') 12 | 13 | @pytest.fixture() 14 | def app(): 15 | app = run() 16 | app.config.update({ 17 | "TESTING": True, 18 | }) 19 | 20 | yield app 21 | 22 | @pytest.fixture() 23 | def client(app): 24 | return app.test_client() 25 | 26 | 27 | @pytest.fixture() 28 | def runner(app): 29 | return app.test_cli_runner() 30 | 31 | @pytest.fixture() 32 | def dev_app(monkeypatch): 33 | monkeypatch.setenv("ENV", "dev") 34 | 35 | app = run() 36 | app.config.update({ 37 | "TESTING": True, 38 | }) 39 | 40 | 41 | 42 | yield app 43 | 44 | 45 | @pytest.fixture() 46 | def dev_client(dev_app): 47 | return dev_app.test_client() 48 | 49 | 50 | @pytest.fixture(scope='function') 51 | def mock_csrf_needed(mocker, app): 52 | mock_csrf_enabled = mocker.patch.object(api.security.csrf.csrf, 'is_csrf_enabled') 53 | mock_csrf_enabled.return_value = False 54 | 55 | @pytest.fixture 56 | def mock_disable_auth(mocker): 57 | mocker.patch.object(api.utils, 'DISABLE_AUTH', True) -------------------------------------------------------------------------------- /api/tests/exceptions/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from botocore.exceptions import ClientError 3 | 4 | 5 | @pytest.fixture() 6 | def client_error_response(): 7 | error_response = dict(Error={'Code': 400, 'Message': 'Operation failed'}) 8 | error_response['ResponseMetadata'] = dict(HTTPStatusCode=400) 9 | return ClientError(error_response, 'failed_operation') 10 | -------------------------------------------------------------------------------- /api/tests/security/csrf/conftest.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | import pytest 4 | from itsdangerous import URLSafeTimedSerializer 5 | 6 | from api.tests.security.csrf.test_csrf import MOCK_URANDOM_VALUE, SECRET_KEY, SALT 7 | 8 | 9 | @pytest.fixture 10 | def mock_csrf_token_value(): 11 | _mock_csrf_token_value = hashlib.sha256(MOCK_URANDOM_VALUE).hexdigest() 12 | return _mock_csrf_token_value 13 | 14 | 15 | @pytest.fixture 16 | def mock_csrf_token_string(mock_csrf_token_value): 17 | return URLSafeTimedSerializer(SECRET_KEY, SALT, signer_kwargs={'digest_method': hashlib.sha256}).dumps(mock_csrf_token_value) 18 | 19 | 20 | @pytest.fixture(scope='function') 21 | def mock_parse_csrf(mocker): 22 | return mocker.patch('api.security.csrf.csrf.parse_csrf_token') 23 | -------------------------------------------------------------------------------- /api/tests/security/test_add_response_headers.py: -------------------------------------------------------------------------------- 1 | from flask import Response 2 | 3 | from api.security.headers import add_security_headers, add_security_headers_dev 4 | 5 | COMMON_SECURITY_HEADERS = { 6 | 'X-Frame-Options': 'DENY', 7 | 'X-Content-Type-Options': 'nosniff', 8 | 'Referrer-Policy': 'strict-origin-when-cross-origin', 9 | 'Strict-Transport-Security': 'max-age=63072000; includeSubDomains; preload', 10 | 'Permissions-Policy': 'interest-cohort=()', 11 | 'X-XSS-Protection': '1; mode=block' 12 | } 13 | 14 | CSP_HEADER = { 15 | 'Content-Security-Policy': "default-src 'self'; style-src 'self' 'unsafe-inline'; font-src data:; img-src 'self' data:; child-src blob:; object-src 'none'; frame-ancestors 'none'; base-uri 'none';" 16 | } 17 | 18 | 19 | def test_response_security_headers_dev(dev_app, dev_client): 20 | """ 21 | Given a PCM app 22 | When a response is processed 23 | Then it should have security headers correctly set for local development 24 | """ 25 | expected_security_headers = COMMON_SECURITY_HEADERS 26 | 27 | response = Response() 28 | response = add_security_headers_dev(response) 29 | 30 | for header, value in expected_security_headers.items(): 31 | assert header in response.headers 32 | assert value == response.headers[header] 33 | 34 | 35 | def test_response_security_headers_prod(app, client): 36 | """ 37 | Given a PCM app 38 | When a response is processed 39 | Then it should have security headers correctly set 40 | """ 41 | expected_security_headers = { 42 | 'Cross-Origin-Resource-Policy': 'same-site', 43 | 'Cross-Origin-Embedder-Policy': 'require-corp', 44 | **CSP_HEADER, 45 | **COMMON_SECURITY_HEADERS 46 | } 47 | 48 | response = Response() 49 | response = add_security_headers(response) 50 | 51 | for header, value in expected_security_headers.items(): 52 | assert header in response.headers 53 | assert value == response.headers[header] 54 | -------------------------------------------------------------------------------- /api/tests/security/test_secret_generator.py: -------------------------------------------------------------------------------- 1 | from api.security.fingerprint import CognitoFingerprintGenerator 2 | 3 | 4 | def test_cognito_fingerprint_generator(): 5 | """ 6 | With fixed values 7 | and a CognitoFingerprintGenerator 8 | it should produce the same fingerprint everytime 9 | """ 10 | client_id, client_secret, user_pool_id = 'client-id', 'client-secret', 'pool-id' 11 | expected_fingerprint = '88056a6f3236e82b05de9bbb01a979b2877563c0c971148bc20aaa9aad8b3b85' 12 | 13 | gen = CognitoFingerprintGenerator(client_id, client_secret, user_pool_id) 14 | fingerprint = gen.fingerprint() 15 | 16 | assert fingerprint == expected_fingerprint 17 | -------------------------------------------------------------------------------- /api/tests/test_logout.py: -------------------------------------------------------------------------------- 1 | import os 2 | from unittest import mock 3 | 4 | import pytest 5 | from werkzeug.utils import redirect 6 | 7 | from api.PclusterApiHandler import logout 8 | 9 | @pytest.fixture 10 | def mock_cognito_redirect(mocker): 11 | mocked_cognito_redirect_url = 'some-url/logout?client_id=client_id&redirect_uri=redirect_uri&response_type=code&scope=scope_list' 12 | mocked_redirect = redirect(mocked_cognito_redirect_url, code=302) 13 | mocker.patch('api.PclusterApiHandler.__cognito_logout_redirect', return_value=mocked_redirect) 14 | 15 | return mocked_cognito_redirect_url 16 | 17 | @pytest.fixture 18 | def mock_revoke_refresh_token(mocker): 19 | return mocker.patch('api.PclusterApiHandler.revoke_cognito_refresh_token') 20 | 21 | def test_logout_redirect(app, mock_cognito_redirect, mock_revoke_refresh_token): 22 | """ 23 | Given a handler for the /logout endpoint 24 | When user logs out 25 | Then it should redirect to index.html 26 | """ 27 | with app.test_request_context(headers={'Cookie': 'accessToken=access-token;refreshToken=refresh-token'}): 28 | res = logout() 29 | 30 | assert res.status_code == 302 31 | assert res.location == mock_cognito_redirect 32 | mock_revoke_refresh_token.assert_called_once_with('refresh-token') 33 | 34 | def test_logout_clear_cookies(app, mock_revoke_refresh_token): 35 | """ 36 | Given an handler for the /logout endpoint 37 | When user logs out 38 | Then it should clear the authentication cookies 39 | """ 40 | with app.test_request_context(headers={'Cookie': 'accessToken=access-token;refreshToken=refresh-token'}): 41 | res = logout() 42 | 43 | cookie_list = res.headers.getlist('Set-Cookie') 44 | assert "accessToken=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/" in cookie_list 45 | assert "idToken=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/" in cookie_list 46 | assert "refreshToken=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/" in cookie_list 47 | assert "csrf=; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/" in cookie_list 48 | mock_revoke_refresh_token.assert_called_once_with('refresh-token') -------------------------------------------------------------------------------- /api/tests/test_pcluster_api_handler.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | from api.PclusterApiHandler import login 3 | 4 | 5 | @mock.patch("api.PclusterApiHandler.requests.post") 6 | def test_on_successful_login_auth_cookies_are_set(mock_post, client): 7 | with client as flaskClient: 8 | response_dict = { 9 | "access_token": "testAccessToken", 10 | "id_token": "testIdToken", 11 | "refresh_token": "testRefreshToken" 12 | } 13 | mock_post.return_value.json.return_value = response_dict 14 | resp = flaskClient.get("/login", query_string="code=testCode") 15 | cookie_list = resp.headers.getlist('Set-Cookie') 16 | assert "accessToken=testAccessToken; Secure; HttpOnly; Path=/; SameSite=Lax" in cookie_list 17 | assert "idToken=testIdToken; Secure; HttpOnly; Path=/; SameSite=Lax" in cookie_list 18 | assert "refreshToken=testRefreshToken; Secure; HttpOnly; Path=/; SameSite=Lax" in cookie_list 19 | 20 | 21 | def test_login_with_no_access_token_returns_401(mocker, app): 22 | with app.test_request_context('/login', query_string='code=testCode'): 23 | mock_abort = mocker.patch('api.PclusterApiHandler.abort') 24 | mock_post = mocker.patch('api.PclusterApiHandler.requests.post') 25 | mock_post.return_value.json.return_value = {'access_token': None} 26 | 27 | login() 28 | 29 | mock_abort.assert_called_once_with(401) 30 | -------------------------------------------------------------------------------- /api/tests/test_push_log.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | def trim_log(caplog): 4 | return caplog.text.replace('\n', '') 5 | 6 | 7 | def test_push_log_controller_with_valid_json_no_extra(client, caplog, mock_disable_auth, mock_csrf_needed): 8 | request_body = { 'logs': [{'message': 'sample-message', 'level': 'info'}] } 9 | expected_log = "INFO pcluster-manager:logger.py:24 {'message': 'sample-message'}" 10 | 11 | caplog.clear() 12 | response = client.post('/logs', json=request_body) 13 | 14 | assert response.status_code == 200 15 | assert expected_log in trim_log(caplog) 16 | 17 | 18 | def test_push_log_controller_with_valid_json_with_extra(client, caplog, mock_disable_auth, mock_csrf_needed): 19 | request_body = { 'logs': [{'message': 'sample-message', 'level': 'error', 20 | 'extra': {'extra_1': 'value_1', 'extra_2': 'value_2'}}]} 21 | expected_log = "ERROR pcluster-manager:logger.py:30 {'extra_1': 'value_1', 'extra_2': 'value_2', 'message': 'sample-message'}" 22 | 23 | caplog.clear() 24 | response = client.post('/logs', json=request_body) 25 | 26 | assert response.status_code == 200 27 | assert expected_log in trim_log(caplog) 28 | -------------------------------------------------------------------------------- /api/tests/test_revoke_cognito_refresh_token.py: -------------------------------------------------------------------------------- 1 | from unittest.mock import call, ANY 2 | 3 | import pytest 4 | 5 | from api.PclusterApiHandler import revoke_cognito_refresh_token 6 | from api.logging.logger import DefaultLogger 7 | 8 | 9 | @pytest.fixture 10 | def mock_requests(mocker): 11 | return mocker.patch('api.PclusterApiHandler.requests') 12 | 13 | @pytest.fixture 14 | def mock_logger(mocker): 15 | return mocker.patch('api.PclusterApiHandler.logger', DefaultLogger(is_running_local=False)) 16 | 17 | class MockResponse: 18 | def __init__(self, status_code): 19 | self.status_code = status_code 20 | 21 | def test_revoke_cognito_refresh_token_success(mock_requests): 22 | mock_requests.post.return_value = MockResponse(200) 23 | 24 | revoke_cognito_refresh_token('refresh-token') 25 | 26 | mock_requests.post.has_calls(call(ANY, {'token': 'refresh-token'}, ANY, {'Content-Type': 'application/x-www-form-urlencoded'})) 27 | 28 | 29 | 30 | 31 | def test_revoke_cognito_refresh_token_failing(mock_requests, mock_logger, caplog): 32 | mock_requests.post.return_value = MockResponse(400) 33 | 34 | revoke_cognito_refresh_token('refresh-token') 35 | 36 | mock_requests.post.has_calls(call(ANY, {'token': 'refresh-token'}, ANY, {'Content-Type': 'application/x-www-form-urlencoded'})) 37 | assert caplog.text.strip() == "WARNING pcluster-manager:logger.py:27 {'message': 'Unable to revoke cognito refresh token'}" -------------------------------------------------------------------------------- /api/tests/validation/test_api_custom_validators.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from marshmallow import ValidationError 3 | 4 | from api.validation.validators import size_not_exceeding 5 | 6 | 7 | def test_size_not_exceeding(): 8 | max_size = 300 9 | test_str_not_exceeding = 'a' * (max_size - 2) # save 2 chars for double quotes 10 | 11 | size_not_exceeding(test_str_not_exceeding, max_size) 12 | 13 | def test_size_not_exceeding_failing(): 14 | max_size = 300 15 | test_str_not_exceeding = 'a' * max_size # will produce "aaa...", max_size + 2 16 | 17 | with pytest.raises(ValidationError): 18 | size_not_exceeding(test_str_not_exceeding, max_size) -------------------------------------------------------------------------------- /api/validation/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | from flask import request, Request 4 | from marshmallow import Schema, ValidationError 5 | 6 | from api.validation.schemas import EC2Action 7 | 8 | 9 | def __validate_request(_request: Request, *, body_schema: Schema = None, params_schema: Schema = None, cookies_schema: Schema = None, raise_on_missing_body = True): 10 | errors = {} 11 | if body_schema: 12 | try: 13 | errors.update(body_schema.validate(_request.json)) 14 | except: 15 | if raise_on_missing_body: 16 | raise ValueError('Expected json body') 17 | 18 | if params_schema: 19 | errors.update(params_schema.validate(_request.args)) 20 | 21 | if cookies_schema: 22 | errors.update(cookies_schema.validate(_request.cookies)) 23 | 24 | return errors 25 | 26 | 27 | def validated(*, body: Schema = None, params: Schema = None, cookies: Schema = None, raise_on_missing_body = True): 28 | def wrapper(func): 29 | @wraps(func) 30 | def decorated(*pargs, **kwargs): 31 | errors = __validate_request(request, body_schema=body, params_schema=params, cookies_schema=cookies, raise_on_missing_body=raise_on_missing_body) 32 | if errors: 33 | raise ValidationError(f'Input validation failed for {request.path}', data=errors) 34 | return func(*pargs, **kwargs) 35 | 36 | return decorated 37 | 38 | return wrapper 39 | -------------------------------------------------------------------------------- /api/validation/validators.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from marshmallow import validate, ValidationError 4 | import re 5 | 6 | from api.logging import VALID_LOG_LEVELS 7 | 8 | # PC available regions 9 | PC_REGIONS = [ 10 | 'us-east-2','us-east-1','us-west-1','us-west-2', 11 | 'af-south-1','ap-east-1','ap-south-1','ap-northeast-2', 12 | 'ap-southeast-1','ap-southeast-2','ap-northeast-1', 13 | 'ca-central-1','cn-north-1','cn-northwest-1', 14 | 'eu-central-1','eu-west-1','eu-west-2', 15 | 'eu-south-1','eu-west-3','eu-north-1','me-south-1', 16 | 'sa-east-1','us-gov-east-1','us-gov-west-1' 17 | ] 18 | 19 | 20 | def comma_splittable(arg: str): 21 | try: 22 | arg.split(',') 23 | return True 24 | except: 25 | return False 26 | 27 | 28 | def is_alphanumeric_with_hyphen(arg: str): 29 | pattern = re.compile(r"^[a-zA-Z][a-zA-Z0-9-]+$") 30 | return bool(re.fullmatch(pattern, arg)) 31 | 32 | 33 | aws_region_validator = validate.OneOf(choices=PC_REGIONS) 34 | 35 | 36 | def valid_api_log_levels_predicate(loglevel): 37 | return loglevel.lower() in VALID_LOG_LEVELS 38 | 39 | def size_not_exceeding(data, size): 40 | bytes_ = bytes(json.dumps(data), 'utf-8') 41 | byte_size = len(bytes_) 42 | if byte_size > size: 43 | raise ValidationError(f'Request body exceeded max size of {size}') -------------------------------------------------------------------------------- /awslambda/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | # with the License. A copy of the License is located at 5 | # 6 | # http://aws.amazon.com/apache2.0/ 7 | # 8 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | -------------------------------------------------------------------------------- /awslambda/entrypoint.py: -------------------------------------------------------------------------------- 1 | # Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | # with the License. A copy of the License is located at 5 | # 6 | # http://aws.amazon.com/apache2.0/ 7 | # 8 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | # limitations under the License. 11 | import os 12 | from os import environ 13 | from typing import Any, Dict 14 | 15 | import app 16 | import logging 17 | 18 | from awslambda.serverless_wsgi import handle_request 19 | 20 | # Initialize as a global to re-use across Lambda invocations 21 | pcluster_manager_api = None # pylint: disable=invalid-name 22 | 23 | profile = environ.get("PROFILE", "prod") 24 | is_dev_profile = profile == "dev" 25 | 26 | if is_dev_profile: 27 | environ["FLASK_ENV"] = "development" 28 | environ["FLASK_DEBUG"] = "1" 29 | 30 | 31 | def _init_flask_app(): 32 | return app.run() 33 | 34 | def lambda_handler(event: Dict[str, Any], context) -> Dict[str, Any]: 35 | try: 36 | global pcluster_manager_api # pylint: disable=global-statement,invalid-name 37 | if not pcluster_manager_api: 38 | logging.info("Initializing Flask Application") 39 | pcluster_manager_api = _init_flask_app() 40 | # Setting default region to region where lambda function is executed 41 | os.environ["AWS_DEFAULT_REGION"] = os.environ["AWS_REGION"] 42 | return handle_request(pcluster_manager_api, event, context) 43 | except Exception as e: 44 | logging.critical("Unexpected exception: %s", e, exc_info=True) 45 | raise Exception("Unexpected fatal exception. Please look at API logs for details on the encountered failure.") 46 | -------------------------------------------------------------------------------- /decisions/20220708-use-log4brains-to-manage-the-adrs.md: -------------------------------------------------------------------------------- 1 | # Use Log4brains to manage the ADRs 2 | 3 | - Status: accepted 4 | - Date: 2022-07-07 5 | - Tags: dev-tools, doc 6 | 7 | ## Context and Problem Statement 8 | 9 | We want to record architectural decisions made in this project. 10 | Which tool(s) should we use to manage these records? 11 | 12 | ## Considered Options 13 | 14 | - [Log4brains](https://github.com/thomvaill/log4brains): architecture knowledge base (command-line + static site generator) 15 | - [ADR Tools](https://github.com/npryce/adr-tools): command-line to create ADRs 16 | - [ADR Tools Python](https://bitbucket.org/tinkerer_/adr-tools-python/src/master/): command-line to create ADRs 17 | - [adr-viewer](https://github.com/mrwilson/adr-viewer): static site generator 18 | - [adr-log](https://adr.github.io/adr-log/): command-line to create a TOC of ADRs 19 | 20 | ## Decision Outcome 21 | 22 | Chosen option: "Log4brains", because it includes the features of all the other tools, and even more. 23 | -------------------------------------------------------------------------------- /decisions/20220708-use-michael-nygard-format-for-adr.md: -------------------------------------------------------------------------------- 1 | # Use Michael Nygard format for ADR 2 | 3 | - Status: accepted 4 | - Tags: dev-tools, doc 5 | 6 | ## Context 7 | Adopting a format for ADR which is too verbose might prevent people from writing them, as they might consider the activity too time consuming. 8 | 9 | ## Decision 10 | We decided to adopt the format proposed by Michael Nygard, as it is brief enough to be written rapidly and conveys enough information for making the adoption of ADR valuable. 11 | 12 | ## Consequences 13 | - People might be more keen to write ADRs as the format is simpler 14 | - We might risk to loose the driver for some decisions and the options evaluated 15 | 16 | ## Links 17 | - [ADR template by Michael Nygard in Markdown](https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/templates/decision-record-template-by-michael-nygard/index.md) -------------------------------------------------------------------------------- /decisions/20220713-use-github-actions-for-pipelines.md: -------------------------------------------------------------------------------- 1 | # Use Github Actions for pipelines 2 | 3 | - Status: accepted 4 | - Deciders: Mattia Franchetto, Alessandro Menduni, Marco Basile 5 | - Date: 7/12/2022 6 | 7 | ## Context 8 | PCluster Manager doesn't have a pipeline to run tests of different kinds (unit, integration and so on) in a continuous integration fashion, and optionally deliver the tested changes automatically. 9 | 10 | ## Decision 11 | Pipelines will be built on top of Github Actions because it's fast, simple to configure using YAML files, immediately available and free for open source projects. 12 | A solution like AWS CodeBuild has been discarded for the moment for its complex setup, but may be used in the future. 13 | 14 | ## Consequences 15 | The pipeline will perform tests on every pull request: if we fail to configure it properly the code won't get merged until the pipeline is restored with manual actions (like restarting the job or tweak the configuration), but this is a common scenario regardless of the tool being used. 16 | 17 | ## Links 18 | - [Actions](https://docs.github.com/en/actions) 19 | -------------------------------------------------------------------------------- /decisions/20221025-public-documentation-release.md: -------------------------------------------------------------------------------- 1 | # Public documentation release 2 | 3 | - Status: accepted 4 | - Date: 2022-10-24 5 | 6 | ## Context 7 | The [public documentation](https://pcluster.cloud/) is updated every time a new pull request is made. This is not always the desired behavior, especially when an amend is made to the documentation for unreleased ParallelCluster or ParallelCluster Manager features. 8 | 9 | ## Decision 10 | Change the documentation workflow's trigger to run only on releases. 11 | 12 | ## Consequences 13 | The documentation is updated only when ParallelCluster Mananager is released to customers. The downside is quick fixes to the documentation require more time to be released. 14 | -------------------------------------------------------------------------------- /decisions/20221027-export-pcluster-manager-logs-from-cloudwatch.md: -------------------------------------------------------------------------------- 1 | # Export PCluster Manager logs from CloudWatch 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe team 5 | - Date: 2022-10-27 6 | - Tags: cloudwatch, logging, s3 7 | 8 | ## Context 9 | Customers need a way to easily export their PCluster Manager deployment logs in order to provide the support team 10 | with the info needed to get help. Right now there is no way from the PCM UI to be guided in performing the 11 | necessary actions to download log files produced by the application. 12 | Since a complete automation and a 1-click experience are not feasible right now with what we have, we cannot provide 13 | customers with an immediate way to get logs and some manual process would still be necessary, 14 | (pre-signed URL for S3 are only possible for single objects and not collection of objects), so right now we decided to 15 | skip the automation part entirely. 16 | 17 | ## Decision 18 | Guide the users with documentation links and/or instructions on how to perform an [export to s3 action](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/S3ExportTasks.html) 19 | letting them know what is needed and how to download log archives. 20 | 21 | ## Consequences 22 | Helping customers solve their issues will become easier and more manageable, 23 | although part of the manual process may be error-prone for users less acquainted with AWS console. 24 | -------------------------------------------------------------------------------- /decisions/20221027-support-multiple-versions-of-pc-api.md: -------------------------------------------------------------------------------- 1 | # Support multiple versions of PC API 2 | 3 | - Status: accepted 4 | - Date: 2022-10-27 5 | - Tags: feature-flags, pc-api 6 | 7 | ## Context 8 | Both the UI and the generated YAML need to change according to version of the PC api the customer is using. There is no central point where all the features we support are listed and it is getting hard to keep track of all the moving parts whenever a new version of the PC api is released 9 | 10 | ## Decision 11 | Use feature flags to toggle on and off features. Generate the list of active flags based on the current version of the PC api. 12 | 13 | ## Consequences 14 | - Upside: We have a single point where every feature is listed, and a single map to manage which feature maps to which version. 15 | - Downside: We still have to maintain different UIs and it may get very hard to test every combination of features in the long run. -------------------------------------------------------------------------------- /decisions/20221107-adopt-github-labels-to-allow-automating-release-changelog-creation.md: -------------------------------------------------------------------------------- 1 | # Adopt Github labels to allow automating release changelog creation 2 | 3 | - Status: superseded by [20221129-adopt-releaselabels-to-automate-changelog-generation](20221129-adopt-releaselabels-to-automate-changelog-generation.md) 4 | - Deciders: Nuraghe team 5 | - Date: 2022-11-07 6 | - Tags: release, changelog, labels 7 | 8 | ## Context 9 | The creation of a release changelog is a manual process involving the repetition of a few steps. 10 | All pull requests need to be checked from the previous release to the latest commit target of the code freeze. 11 | While manually reviewing PRs we also need to sort them in one of multiple lists, like "Bugfixes", "New features", etc. 12 | This is a pretty long process and error-prone, since to the human eye things get lost easily. 13 | 14 | ## Decision 15 | We are adopting a simple labeling strategy to select which PRs are worthy of being mentioned in the changelog. 16 | Right now only two labels exist: 17 | 18 | - release-include (to include the target PR in the generated changelog in a section named "Features") 19 | - release-exclude (to explicitly exclude the target PR from the generated changelog) 20 | 21 | Other tags that cause the PR to be excluded are: 22 | 23 | - DON'T MERGE 24 | - duplicate 25 | - invalid 26 | - question 27 | - wontfix 28 | 29 | ## Commitment 30 | We commit to two things: 31 | 32 | - to write better PR titles if the PR is intended to be included in the generated changelog 33 | - (after a first test with the next release) to use more labels of the likes of: 34 | 35 | - release:breaking-change 36 | - release:bugfix 37 | - release:improvement 38 | - release:feature 39 | 40 | To include the PR in a separate section of the generated changelog 41 | 42 | ## Consequences 43 | What becomes easier or more difficult to do because of this change? 44 | 45 | ## Links 46 | - [Automatically generated release notes](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes) 47 | -------------------------------------------------------------------------------- /decisions/20221129-adopt-releaselabels-to-automate-changelog-generation.md: -------------------------------------------------------------------------------- 1 | # Adopt release:*labels to automate Changelog generation 2 | 3 | - Status: accepted 4 | - Deciders: Marco Basile 5 | - Tags: release changelog labels 6 | 7 | ## Context 8 | In the superseded ADR, it had been decided to automatically generate the changelog with a simple release-include/exclude labeling system. In order to generate a more useful changelog for our users, we committed to improve on this mechanism 9 | 10 | ## Decision 11 | We are adopting the following labels for our PRs: 12 | 13 | - release:breaking-change 14 | - release:bugfix 15 | - release:improvement 16 | - release:feature 17 | - release:deprecated 18 | 19 | ## Links 20 | - Supersedes [20221107-adopt-github-labels-to-allow-automating-release-changelog-creation](20221107-adopt-github-labels-to-allow-automating-release-changelog-creation.md) 21 | -------------------------------------------------------------------------------- /decisions/20221206-frontend-log-instrumentation.md: -------------------------------------------------------------------------------- 1 | # Frontend log instrumentation 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe team 5 | - Tags: logs, frontend 6 | 7 | ## Context 8 | Right now we only have a remote logger implementation that pushes logs to the backend. 9 | We want to also log stacktraces but without source-maps it would be hard for those stacktraces 10 | to be actually useful/readable. 11 | 12 | ## Decision 13 | Since our logger implementation provides an `extra` parameter for additional info to add to the log entry, we will: 14 | 15 | - make an effort to log clear and effective messages that are actually useful when read by a developer/technician 16 | - continuously look for ways to improve our existing logging calls when needed 17 | - optionally leverage the `extra` parameter that to provide contextual information about where the log entry is being logged from 18 | 19 | ## Consequences 20 | We get the ability to have effective logs available for inspection in case it's needed. 21 | 22 | ## Useful Links 23 | - [Owasp Logging](https://cheatsheetseries.owasp.org/cheatsheets/Logging_Cheat_Sheet.html) 24 | - [9 best practices for logging](https://www.atatus.com/blog/9-best-practice-for-application-logging-that-you-must-know/#9-logging-and-monitoring-best-practices) 25 | - [13 best practices for logging](https://www.dataset.com/blog/the-10-commandments-of-logging/) -------------------------------------------------------------------------------- /decisions/20230125-pcui-versioning-strategy.md: -------------------------------------------------------------------------------- 1 | # PCUI Versioning strategy 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe Team 5 | - Tags: versioning, pcui 6 | 7 | ## Context 8 | We want to avoid confusion for customers using PC UI and Parallel Cluster. 9 | So we need to keep PC UI and PC versioning schemas separated. 10 | While PC keeps their semver-like schema, PC UI switches do a year.month[.revision] schema. 11 | 12 | 13 | ## Decision 14 | Using the format YYYY.MM[.REVISION] for PC UI versions, where 15 | 16 | - YYYY is the full year in which PC UI gets released 17 | - MM is the two digit number for the month in which PC UI gets released 18 | - REVISION is an optional value to allow separation of patches between the same major version 19 | 20 | -------------------------------------------------------------------------------- /decisions/README.md: -------------------------------------------------------------------------------- 1 | # Architecture Decision Records 2 | 3 | ## Development 4 | 5 | If not already done, install Log4brains: 6 | 7 | ```bash 8 | npm install -g log4brains 9 | ``` 10 | 11 | To preview the knowledge base locally, run: 12 | 13 | ```bash 14 | log4brains preview 15 | ``` 16 | 17 | In preview mode, the Hot Reload feature is enabled: any change you make to a markdown file is applied live in the UI. 18 | 19 | To create a new ADR interactively, run: 20 | 21 | ```bash 22 | log4brains adr new 23 | ``` 24 | 25 | ## More information 26 | 27 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) 28 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) 29 | - [ADR GitHub organization](https://adr.github.io/) 30 | -------------------------------------------------------------------------------- /decisions/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Architecture knowledge base 4 | 5 | Welcome 👋 to the architecture knowledge base of AWS ParallelCluster UI (PCUI). 6 | You will find here all the Architecture Decision Records (ADR) of the project. 7 | 8 | ## Definition and purpose 9 | 10 | > An Architectural Decision (AD) is a software design choice that addresses a functional or non-functional requirement that is architecturally significant. 11 | > An Architectural Decision Record (ADR) captures a single AD, such as often done when writing personal notes or meeting minutes; the collection of ADRs created and maintained in a project constitutes its decision log. 12 | 13 | An ADR is immutable: only its status can change (i.e., become deprecated or superseded). That way, you can become familiar with the whole project history just by reading its decision log in chronological order. 14 | Moreover, maintaining this documentation aims at: 15 | 16 | - 🚀 Improving and speeding up the onboarding of a new team member 17 | - 🔭 Avoiding blind acceptance/reversal of a past decision (cf [Michael Nygard's famous article on ADRs](https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions.html)) 18 | - 🤝 Formalizing the decision process of the team 19 | 20 | ## Usage 21 | 22 | This website is automatically updated after a change on the `master` branch of the project's Git repository. 23 | In fact, the developers manage this documentation directly with markdown files located next to their code, so it is more convenient for them to keep it up-to-date. 24 | You can browse the ADRs by using the left menu or the search bar. 25 | 26 | The typical workflow of an ADR is the following: 27 | 28 | ![ADR workflow](/l4b-static/adr-workflow.png) 29 | 30 | The decision process is entirely collaborative and backed by pull requests. 31 | 32 | ## More information 33 | 34 | - [Log4brains documentation](https://github.com/thomvaill/log4brains/tree/master#readme) 35 | - [What is an ADR and why should you use them](https://github.com/thomvaill/log4brains/tree/master#-what-is-an-adr-and-why-should-you-use-them) 36 | - [ADR GitHub organization](https://adr.github.io/) 37 | -------------------------------------------------------------------------------- /decisions/template.md: -------------------------------------------------------------------------------- 1 | # [short title of solved problem and solution] 2 | 3 | - Status: [draft | proposed | rejected | accepted | deprecated | … | superseded by [xxx](yyyymmdd-xxx.md)] 4 | - Deciders: [list everyone involved in the decision] 5 | - Date: [YYYY-MM-DD when the decision was last updated] 6 | - Tags: [space and/or comma separated list of tags] 7 | 8 | ## Context 9 | What is the issue that we're seeing that is motivating this decision or change? 10 | 11 | ## Decision 12 | What is the change that we're proposing and/or doing? 13 | 14 | ## Consequences 15 | What becomes easier or more difficult to do because of this change? 16 | 17 | ## Links 18 | - [Link type](link to adr) 19 | - … 20 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | public -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | pcluster.cloud 2 | ================================ 3 | 4 | This project contains the documentation reachable at [pcluster.cloud](https://pcluster.cloud). It is built with [Hugo](https://gohugo.io/) 5 | 6 | ## Getting Started 7 | 8 | In order to contribute to the documentation, make sure you set up the following: 9 | 10 | - [install hugo](https://gohugo.io/installation/) 11 | - checkout the submodule with `git submodule update` 12 | - start the development server with `hugo server -D` -------------------------------------------------------------------------------- /docs/archetypes/default.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "{{ replace .Name "-" " " | title }}" 3 | date: {{ .Date }} 4 | draft: true 5 | --- 6 | 7 | -------------------------------------------------------------------------------- /docs/config.toml: -------------------------------------------------------------------------------- 1 | baseURL = "https://pcluster.cloud" 2 | languageCode = 'en-us' 3 | title = 'Pcluster Manager Docs' 4 | theme = "hugo-theme-learn" 5 | uglyurls = true 6 | sectionPagesMenu = "main" 7 | pygmentsCodeFences = true 8 | 9 | [blackfriday] 10 | hrefTargetBlank = true 11 | 12 | [params] 13 | editURL = "https://github.com/aws-samples/pcluster-manager/tree/main/docs/content/" 14 | themeVariant = "mine" 15 | showVisitedLinks = true 16 | description = "Pcluster Manager Docs" 17 | disableSearch = false 18 | disableAssetsBusting = false 19 | disableLanguageSwitchingButton = true 20 | disableShortcutsTitle = false 21 | disableLandingPageButton = true 22 | 23 | # For search functionality 24 | [outputs] 25 | home = [ "HTML", "RSS", "JSON"] 26 | 27 | [[menu.shortcuts]] 28 | name = " Github" 29 | url = "https://github.com/aws-samples/pcluster-manager" 30 | weight = 10 31 | 32 | [[menu.shortcuts]] 33 | name = " AWS - HPC" 34 | url = "https://aws.amazon.com/hpc/" 35 | weight = 10 36 | -------------------------------------------------------------------------------- /docs/content/01-getting-started/01-aws-console-login.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "a. Access AWS" 3 | date = 2019-09-18T10:46:30-04:00 4 | draft = false 5 | weight = 11 6 | +++ 7 | 8 | Depending on your workshop, you may access the AWS Management Console through direct sign-in ([here](https://signin.aws.amazon.com/console)) or as directed by your trainer. To sign in, enter your AWS Account ID or alias, IAM user name, and password that was provided to you for this lab. 9 | 10 | ![AWS Management Console](login.png) 11 | 12 | After you sign in, take a few minutes to explore the navigation components of the AWS Management Console. 13 | 14 | - A search bar allows you to quickly locate services based on text. 15 | - Recently visited services are located below the search bar. 16 | - In the toolbar, the Services drop-down menu populates a list of all services. 17 | - The Support drop-down menu includes links to support and [documentation](https://docs.aws.amazon.com). 18 | - The Region drop-down menu allows you to select a specific AWS Region. 19 | 20 | Start this workshop by selecting an **AWS Region**: 21 | 22 | Choose the **Region** drop-down menu, then choose **US East (N. Virginia)** us-east-1. 23 | 24 | ![AWS Management Console](aws-console.png) 25 | -------------------------------------------------------------------------------- /docs/content/01-getting-started/03-pcmanager-connect.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "c. Connect to Pcluster Manager" 3 | weight = 13 4 | +++ 5 | 6 | 1. During deployment you received an email titled **[PclusterManager] Welcome to Pcluster Manager, please verify your account.**. Click on the link to login with the temporary code provided. 7 | 8 | ![Pcluster Manager](pcm-email.png) 9 | 10 | 2. **Enter the credentials** using the *email* you used when deploying the stack and the *temporary password* from the email above. 11 | 12 | ![Pcluster Manager CloudFormation Stack](pcmanager-creds.png) 13 | 14 | 3. You will be asked to provide a new password. Enter a new password to complete signup. 15 | 16 | ![Signup Screen](signup.png) 17 | 18 | Congrats! You are ready to create your HPC cluster in AWS. Let's do that in the next section. 19 | 20 | {{% notice note %}} 21 | To get the URL outside of the email, go to [**AWS CloudFormation**](https://console.aws.amazon.com/cloudformation/home) > **pcluster-manager** > **Outputs** then click on the **PclusterManagerUrl** to connect. 22 | ![Pcluster Manager Deployed](pcmanager-url.png) 23 | {{% /notice %}} -------------------------------------------------------------------------------- /docs/content/01-getting-started/04-summary.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "d. Summary" 3 | weight = 14 4 | +++ 5 | 6 | In this part of the lab you deployed the AWS ParallelCluster API and Pcluster Manager. You should see a screen that looks like: 7 | 8 | ![Main Page](pcmanager-first-page.png) -------------------------------------------------------------------------------- /docs/content/01-getting-started/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Setup" 3 | date: 2019-01-24T09:05:54Z 4 | weight: 10 5 | pre: "I ⁃ " 6 | --- 7 | 8 | This workshop walks you through setting up Pcluster Manager. You learn how to navigate the AWS Management Console, access relevant services, and how to deploy a basic infrastructure. 9 | Specifically, you learn how to: 10 | 11 | - Sign in to the AWS Management Console and explore it. 12 | - Deploy Pcluster Manager 13 | - Connect to the Web UI 14 | 15 | You can proceed to the next stage of the workshop once the preparation completed and the stacks deployed. 16 | 17 | {{% notice note %}} 18 | Some pages are long and you will need to scroll down to read all the instructions. 19 | {{% /notice %}} 20 | -------------------------------------------------------------------------------- /docs/content/02-tutorials/01-setup-mfa.md: -------------------------------------------------------------------------------- 1 | +++ 2 | title = "a. Setup MFA 📱" 3 | date = 2019-09-18T10:46:30-04:00 4 | draft = false 5 | weight = 21 6 | +++ 7 | 8 | To enable Multi-Factor Authentication (MFA) with Pcluster Manager there's two steps that need to be completed: 9 | 10 | 1. Enable MFA for Cognito Userpool 11 | 2. Login and setup Authenticator app 12 | 13 | #### 1. Setup MFA in Cognito Userpool 14 | 15 | 1. Navigate to [Cognito Console](https://console.aws.amazon.com/cognito/v2/idp/user-pools/) > Click on your user pool > Select **Sign-in experience** tab > Scroll down to Multi-Factor Authentication and click **Edit** 16 | 2. Select **Require MFA** and **Authenticator apps** > Save changes 17 | 18 | ![PCM MFA Cognito Setup](01-setup-mfa/cognito-enable-mfa.png) 19 | 20 | #### 2. Login to Pcluster Manager 21 | 22 | 1. The next time you login to ParallelCluster Manager you'll see a screen like the following: 23 | 2. Scan the QR code and continue setup in your favorite authenticator app. I reccomend [Authy](https://authy.com/features/setup/). 24 | 25 | ![Setup Authy](01-setup-mfa/setup-authy.png) 26 | -------------------------------------------------------------------------------- /docs/content/02-tutorials/06-downloading.md: -------------------------------------------------------------------------------- 1 | 2 | +++ 3 | title = "f. Downloading ⇓" 4 | weight = 26 5 | +++ 6 | 7 | This tutorial shows you how you can download a file from an external source at cluster start. 8 | 9 | ## Setup 10 | 11 | In order to download a file when you start your cluster, 12 | 13 | 1. From the Pcluster Manager GUI create a new cluster. Select the **Wizard** option: 14 | 2. On the **HeadNode** configuration tab 15 | + Expand **Advanced Options** > turn on **Multi-Script Runner** > select **Downloader** 16 | 17 | ![Downloader Screenshot](06-downloading/downloader.png) 18 | 19 | *Note*: You can specify either an `http://`, `https://` or `s3://` endpoint, however for any s3 location it must reside in the same region as the cluster. 20 | 21 | *Note*: You may specify any number of additional arguments to the script and it will download each of them to the destination directory. 22 | 23 | 24 | 3. On the final screen review and make sure your config looks similar to the following. The only required parameter is the `downloader.sh` script. 25 | 26 | ```yaml 27 | HeadNode: 28 | InstanceType: t2.micro 29 | Ssh: 30 | KeyName: keypair 31 | Networking: 32 | SubnetId: subnet-123456789 33 | CustomActions: 34 | OnNodeConfigured: 35 | Script: >- 36 | https://raw.githubusercontent.com/aws-samples/pcluster-manager/main/resources/scripts/multi-runner.py 37 | Args: 38 | - >- 39 | https://raw.githubusercontent.com/aws-samples/pcluster-manager/main/resources/scripts/downloader.sh 40 | - '-/tmp' 41 | - '-https://aws.amazon.com' 42 | - '-s3://mybucket/myfile' 43 | Scheduling: 44 | Scheduler: slurm 45 | SlurmQueues: 46 | - Name: queue0 47 | ComputeResources: 48 | - Name: queue0-t2-micro 49 | MinCount: 0 50 | MaxCount: 4 51 | Instances: 52 | - InstanceType: t2.micro 53 | Networking: 54 | SubnetIds: 55 | - subnet-123456789 56 | Region: us-east-2 57 | Image: 58 | Os: alinux2 59 | ``` 60 | 61 | 4. Create your cluster, when your cluster is created each of the files will be downloaded into the destination directory. 62 | -------------------------------------------------------------------------------- /docs/content/02-tutorials/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "Tutorials" 3 | date: 2019-01-24T09:05:54Z 4 | weight: 20 5 | pre: "II ⁃ " 6 | --- 7 | 8 | 9 | | Tutorial | Description | 10 | |----------------------------------|-----------------------------------------------------------| 11 | | 📱 [Multi-Factor Authentication](02-tutorials/01-setup-mfa.html) | Setup Multi-Factor Authentication for user login. | 12 | | 🪄 [Slurm Accounting](02-tutorials/02-slurm-accounting.html) | Track job duration, cost, instance type by user. | 13 | | 💾 [Memory Scheduling](02-tutorials/03-memory-scheduling.html) | Schedule using the `--mem` slurm flag. | 14 | | 💰 [Cost Tags](02-tutorials/04-cost-tracking.html) | Track job costs in AWS Cost Explorer by user and project. | 15 | | ⇓ [Downloading](02-tutorials/06-downloading.html) | Download files at cluster start. | 16 | | 🔑 [Setup IAM Permissions](02-tutorials/07-setup-iam.html) | Setup IAM permissions needed to attach additional policies. | 17 | | 🔗 [Setup Custom Domain](02-tutorials/08-custom-domain.html) | Setup a custom domain. | 18 | -------------------------------------------------------------------------------- /docs/static/01-getting-started/aws-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/aws-console.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/cfn-outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/cfn-outputs.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/create.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/login.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/login.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/main-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/main-page.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcm-email.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcm-email.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-creds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-creds.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-deploy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-deploy.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-deployed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-deployed.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-first-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-first-page.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-install.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-install.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-stack.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/pcmanager-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/pcmanager-url.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/signup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/signup.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-cfn-outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-cfn-outputs.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-cfn-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-cfn-properties.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-cluster-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-cluster-properties.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-headnode-additional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-headnode-additional.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-headnode-subnet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-headnode-subnet.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-headnode-virtual-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-headnode-virtual-console.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-job-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-job-details.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-job-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-job-list.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-policy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-policy.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-submit-job-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-submit-job-dialog.png -------------------------------------------------------------------------------- /docs/static/01-getting-started/slurm-accounting-submit-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/01-getting-started/slurm-accounting-submit-job.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/01-setup-mfa/cognito-enable-mfa.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/01-setup-mfa/cognito-enable-mfa.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/01-setup-mfa/setup-authy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/01-setup-mfa/setup-authy.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/additional-sg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/additional-sg.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/architecture.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/attach-admin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/attach-admin.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/attach-policies.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/attach-policies.jpeg -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/attach-policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/attach-policies.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/cfn-outputs.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/cfn-outputs.jpeg -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/cfn-outputs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/cfn-outputs.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/cfn-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/cfn-properties.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/cluster-properties.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/cluster-properties.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/headnode-additional.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/headnode-additional.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/headnode-additionals.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/headnode-additionals.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/headnode-virtual-console.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/headnode-virtual-console.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/job-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/job-details.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/job-list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/job-list.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/lambda-permissions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/lambda-permissions.jpeg -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/pcluster/cluster-config.yaml.template: -------------------------------------------------------------------------------- 1 | Region: @AWS_REGION@ 2 | Image: 3 | Os: alinux2 4 | HeadNode: 5 | InstanceType: t3.xlarge 6 | Networking: 7 | SubnetId: @PUBLIC_SUBNET_ID@ 8 | AdditionalSecurityGroups: 9 | - @SLURM_DB_SG@ 10 | Ssh: 11 | KeyName: @KEY_NAME@ 12 | Iam: 13 | S3Access: 14 | - BucketName: @CONFIG_BUCKET@ 15 | EnableWriteAccess: true 16 | AdditionalIamPolicies: 17 | - Policy: arn:aws:iam::aws:policy/SecretsManagerReadWrite 18 | - Policy: arn:aws:iam::aws:policy/AWSPriceListServiceFullAccess 19 | - Policy: arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore 20 | CustomActions: 21 | OnNodeConfigured: 22 | Script: s3://@CONFIG_BUCKET@/post-install.sh 23 | Args: 24 | - "s3://@CONFIG_BUCKET@" 25 | - "@SECRET_ID@" 26 | - "@RDS_ENDPOINT@" 27 | - "@RDS_PORT@" 28 | Scheduling: 29 | Scheduler: slurm 30 | SlurmQueues: 31 | - Name: queue1 32 | ComputeResources: 33 | - Name: t2micro 34 | Instances: 35 | - InstanceType: t2.micro 36 | MinCount: 0 37 | MaxCount: 10 38 | Networking: 39 | SubnetIds: 40 | - @PRIVATE_SUBNET_ID@ 41 | -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/pcluster/post-install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | # Install MySQL 7 | yum install -y mysql 8 | 9 | # Set variables from post-install args 10 | config_bucket=$1 11 | secret_id=$2 12 | rds_endpoint=$3 13 | rds_port=$4 14 | 15 | # Get Slurm database credentials 16 | slurm_db_user=$(aws secretsmanager get-secret-value --secret-id ${secret_id} --region eu-west-1 | jq --raw-output '.SecretString' | jq -r .username) 17 | slurm_db_password=$(aws secretsmanager get-secret-value --secret-id ${secret_id} --region eu-west-1 | jq --raw-output '.SecretString' | jq -r .password) 18 | 19 | # Other variables needed for configuring Slurm 20 | slurm_dbd_host=$(hostname) 21 | slurm_etc=/opt/slurm/etc 22 | 23 | # Copy Slurm configuration files 24 | aws s3 cp --quiet ${config_bucket}/sacct/slurm_sacct.conf ${slurm_etc}/slurm_sacct.conf 25 | aws s3 cp --quiet ${config_bucket}/sacct/slurmdbd.conf ${slurm_etc}/slurmdbd.conf 26 | 27 | # Modify Slurm configuration files 28 | sed -i "s|@SLURM_DBD_HOST@|${slurm_dbd_host}|g" ${slurm_etc}/slurm_sacct.conf 29 | sed -i "s|@SLURM_DBD_USER@|${slurm_db_user}|g" ${slurm_etc}/slurm_sacct.conf 30 | sed -i "s|@SLURM_DBD_HOST@|${slurm_dbd_host}|g" ${slurm_etc}/slurmdbd.conf 31 | sed -i "s|@RDS_USER@|${slurm_db_user}|g" ${slurm_etc}/slurmdbd.conf 32 | sed -i "s|@RDS_PASS@|${slurm_db_password}|g" ${slurm_etc}/slurmdbd.conf 33 | sed -i "s|@RDS_ENDPOINT@|${rds_endpoint}|g" ${slurm_etc}/slurmdbd.conf 34 | sed -i "s|@RDS_PORT@|${rds_port}|g" ${slurm_etc}/slurmdbd.conf 35 | 36 | echo "include slurm_sacct.conf" >> ${slurm_etc}/slurm.conf 37 | chmod 600 ${slurm_etc}/slurmdbd.conf 38 | chown slurm:slurm ${slurm_etc}/slurmdbd.conf 39 | 40 | # Restart Slurm daemons 41 | /opt/slurm/sbin/slurmdbd 42 | systemctl restart slurmctld 43 | -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/pcluster/sacct/slurm_sacct.conf: -------------------------------------------------------------------------------- 1 | # 2 | # ACCOUNTING 3 | JobAcctGatherType=jobacct_gather/linux 4 | JobAcctGatherFrequency=30 5 | # 6 | AccountingStorageType=accounting_storage/slurmdbd 7 | AccountingStorageHost=@SLURM_DBD_HOST@ 8 | AccountingStorageUser=@SLURM_DBD_USER@ 9 | AccountingStoragePort=6819 10 | AccountingStoreFlags=job_comment,job_env,job_script 11 | AccountingStorageEnforce=assocation,limits,qos 12 | -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/pcluster/sacct/slurmdbd.conf: -------------------------------------------------------------------------------- 1 | # 2 | # Slurm Database Daemon configuration 3 | # 4 | ArchiveEvents=yes 5 | ArchiveJobs=yes 6 | ArchiveResvs=yes 7 | ArchiveSteps=yes 8 | ArchiveSuspend=yes 9 | ArchiveTXN=yes 10 | ArchiveUsage=yes 11 | AuthType=auth/munge 12 | DbdHost=@SLURM_DBD_HOST@ 13 | DbdPort=6819 14 | DebugLevel=info 15 | PurgeEventAfter=1month 16 | PurgeJobAfter=12month 17 | PurgeResvAfter=1month 18 | PurgeStepAfter=1month 19 | PurgeSuspendAfter=1month 20 | PurgeTXNAfter=12month 21 | PurgeUsageAfter=24month 22 | SlurmUser=slurm 23 | LogFile=/var/log/slurmdbd.log 24 | PidFile=/var/run/slurmdbd.pid 25 | PrivateData=accounts,events,jobs,reservations,usage,users 26 | StorageType=accounting_storage/mysql 27 | StorageUser=@RDS_USER@ 28 | StoragePass=@RDS_PASS@ 29 | StorageHost=@RDS_ENDPOINT@ 30 | StoragePort=@RDS_PORT@ 31 | -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/submit-job-dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/submit-job-dialog.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/02-slurm-accounting/submit-job.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/02-slurm-accounting/submit-job.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/04-cost-tracking/cost-explorer-project.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/04-cost-tracking/cost-explorer-project.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/04-cost-tracking/cost-tags-computefleet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/04-cost-tracking/cost-tags-computefleet.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/04-cost-tracking/cost-tags-headnode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/04-cost-tracking/cost-tags-headnode.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/04-cost-tracking/create-budget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/04-cost-tracking/create-budget.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9-1.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9-2.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9-3.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9-4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9-4.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9-5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9-5.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/05-cloud9/cloud9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/05-cloud9/cloud9.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/06-downloading/downloader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/06-downloading/downloader.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/07-setup-iam/attach-policies.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/07-setup-iam/attach-policies.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/07-setup-iam/lambda-permissions.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/07-setup-iam/lambda-permissions.jpeg -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/api-mappings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/api-mappings.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/certificate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/certificate.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/cognito-custom-domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/cognito-custom-domain.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/create-domain.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/create-domain.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/hosted-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/hosted-ui.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/08-custom-domain/site-url.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/08-custom-domain/site-url.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/create.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/create.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/memory-scheduling/HeadNode-Setup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/memory-scheduling/HeadNode-Setup.png -------------------------------------------------------------------------------- /docs/static/02-tutorials/memory-scheduling/memory.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/02-tutorials/memory-scheduling/memory.png -------------------------------------------------------------------------------- /docs/static/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/docs/static/architecture.png -------------------------------------------------------------------------------- /e2e/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /test-results/ 3 | /playwright-report/ 4 | /playwright/.cache/ -------------------------------------------------------------------------------- /e2e/configs/environment.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | export const ENVIRONMENT_CONFIG = { 12 | URL: process.env.E2E_TEST_URL || 'https://ind9uoiv6j.execute-api.eu-west-1.amazonaws.com' 13 | } -------------------------------------------------------------------------------- /e2e/configs/login.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | export const LOGIN_CONFIG = { 12 | username: process.env.E2E_TEST1_EMAIL || '', 13 | password: process.env.E2E_TEST1_PASSWORD || '' 14 | } -------------------------------------------------------------------------------- /e2e/fixtures/wizard.template.yaml: -------------------------------------------------------------------------------- 1 | HeadNode: 2 | InstanceType: t2.micro 3 | Networking: 4 | SubnetId: subnet-006e97a9837b1710d 5 | LocalStorage: 6 | RootVolume: 7 | VolumeType: gp3 8 | Scheduling: 9 | Scheduler: slurm 10 | SlurmQueues: 11 | - Name: queue0 12 | ComputeResources: 13 | - Name: queue0-compute-resource-0 14 | InstanceType: c5n.large 15 | MinCount: 0 16 | MaxCount: 4 17 | Networking: 18 | SubnetIds: 19 | - subnet-006e97a9837b1710d 20 | ComputeSettings: 21 | LocalStorage: 22 | RootVolume: 23 | VolumeType: gp3 24 | Region: ca-central-1 25 | Image: 26 | Os: alinux2 27 | 28 | -------------------------------------------------------------------------------- /e2e/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "e2e", 3 | "scripts": { 4 | "e2e:test": "playwright test" 5 | }, 6 | "devDependencies": { 7 | "@playwright/test": "^1.28.1", 8 | "@types/node": "^18.11.13" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /e2e/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PlaywrightTestConfig } from '@playwright/test'; 2 | import { devices } from '@playwright/test'; 3 | 4 | /** 5 | * See https://playwright.dev/docs/test-configuration. 6 | */ 7 | const config: PlaywrightTestConfig = { 8 | testDir: './specs', 9 | /* Maximum time one test can run for. */ 10 | timeout: 180 * 1000, 11 | expect: { 12 | /** 13 | * Maximum time expect() should wait for the condition to be met. 14 | * For example in `await expect(locator).toHaveText();` 15 | */ 16 | timeout: 10000 17 | }, 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: 'html', 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ 31 | actionTimeout: 15000, 32 | /* Base URL to use in actions like `await page.goto('/')`. */ 33 | // baseURL: 'http://localhost:3000', 34 | 35 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 36 | trace: 'on-first-retry', 37 | video: 'on-first-retry', 38 | screenshot: 'only-on-failure', 39 | }, 40 | 41 | /* Configure projects for major browsers */ 42 | projects: [ 43 | { 44 | name: 'chromium', 45 | use: { 46 | ...devices['Desktop Chrome'], 47 | }, 48 | }, 49 | 50 | { 51 | name: 'firefox', 52 | use: { 53 | ...devices['Desktop Firefox'], 54 | }, 55 | }, 56 | 57 | { 58 | name: 'webkit', 59 | use: { 60 | ...devices['Desktop Safari'], 61 | }, 62 | }, 63 | 64 | ], 65 | 66 | }; 67 | 68 | export default config; 69 | -------------------------------------------------------------------------------- /e2e/specs/noMatch.spec.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import { expect, test } from '@playwright/test'; 12 | import { visitAndLogin } from '../test-utils/login'; 13 | import { ENVIRONMENT_CONFIG } from "../configs/environment"; 14 | 15 | test.describe('Given an endpoint where AWS ParallelCluster UI is deployed', () => { 16 | test.describe('when a user is logged in, and navigates to an unmatched route', () => { 17 | test('an error page should be displayed', async ({ page }) => { 18 | await visitAndLogin(page) 19 | await page.goto(`${ENVIRONMENT_CONFIG.URL}/noMatch`) 20 | await expect(page.getByRole('heading', { name: 'Page not found' })).toBeVisible() 21 | }); 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /e2e/test-utils/login.ts: -------------------------------------------------------------------------------- 1 | import { ENVIRONMENT_CONFIG } from "../configs/environment"; 2 | import { LOGIN_CONFIG } from "../configs/login"; 3 | 4 | export async function visitAndLogin(page) { 5 | await page.goto(ENVIRONMENT_CONFIG.URL); 6 | await page.getByRole('textbox', { name: 'name@host.com' }).fill(LOGIN_CONFIG.username); 7 | await page.getByRole('textbox', { name: 'Password' }).fill(LOGIN_CONFIG.password); 8 | await page.getByRole('button', { name: 'submit' }).click(); 9 | } -------------------------------------------------------------------------------- /frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .next 3 | .swc 4 | build -------------------------------------------------------------------------------- /frontend/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "next/core-web-vitals", 4 | "prettier" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /frontend/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | "$(dirname -- "$0")"/../scripts/git-secrets-command.sh --commit_msg_hook -- "$@" 5 | -------------------------------------------------------------------------------- /frontend/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | "$(dirname -- "$0")"/../scripts/git-secrets-command.sh --pre_commit_hook -- "$@" 5 | npx lint-staged 6 | -------------------------------------------------------------------------------- /frontend/.husky/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | "$(dirname -- "$0")"/../scripts/git-secrets-command.sh --prepare_commit_msg_hook -- "$@" 5 | -------------------------------------------------------------------------------- /frontend/.nvmrc: -------------------------------------------------------------------------------- 1 | 16 -------------------------------------------------------------------------------- /frontend/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "avoid", 3 | "bracketSpacing": false, 4 | "endOfLine": "lf", 5 | "htmlWhitespaceSensitivity": "css", 6 | "jsxBracketSameLine": false, 7 | "printWidth": 80, 8 | "proseWrap": "preserve", 9 | "requirePragma": false, 10 | "semi": false, 11 | "singleQuote": true, 12 | "tabWidth": 2, 13 | "trailingComma": "all", 14 | "useTabs": false 15 | } -------------------------------------------------------------------------------- /frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16-alpine 2 | WORKDIR /app 3 | COPY . . 4 | COPY resources/attributions/npm-python-attributions.txt public/license.txt 5 | # The env var is used to skip git-secrets checks, not needed for the build 6 | ENV CI true 7 | RUN npm ci 8 | # Disable telemetry 9 | # Next.js collects completely anonymous telemetry data about general usage. 10 | # Learn more here: https://nextjs.org/telemetry 11 | ENV NEXT_TELEMETRY_DISABLED 1 12 | RUN npm run export 13 | -------------------------------------------------------------------------------- /frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | const merge = require('lodash/merge'); 2 | const nextJest = require('next/jest') 3 | const awsuiPreset = require('@cloudscape-design/jest-preset/jest-preset'); 4 | 5 | const createJestConfig = nextJest({ 6 | // Provide the path to your Next.js app to load next.config.js and .env files in your test environment 7 | dir: './', 8 | }) 9 | 10 | // Add any custom config to be passed to Jest 11 | const customJestConfig = { 12 | // Add more setup options before each test is run 13 | // setupFilesAfterEnv: ['/jest.setup.js'], 14 | // if using TypeScript with a baseUrl set to the root directory then you need the below for alias' to work 15 | moduleDirectories: ['node_modules', '/'], 16 | testEnvironment: 'jest-environment-jsdom', 17 | } 18 | 19 | async function mergePolarisPreset() { 20 | const nextConfig = await createJestConfig(customJestConfig)() 21 | const mergedConfig = merge({}, nextConfig, awsuiPreset) 22 | 23 | return mergedConfig 24 | } 25 | 26 | module.exports = mergePolarisPreset -------------------------------------------------------------------------------- /frontend/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /frontend/next.config.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | /** @type {import('next').NextConfig} */ 12 | const nextConfig = { 13 | reactStrictMode: true, 14 | experimental: { 15 | images: { 16 | unoptimized: true, 17 | }, 18 | }, 19 | async rewrites() { 20 | return [ 21 | /** 22 | * Rewrite everything to `pages/index` 23 | * 24 | * This is only here because as of yet we are not 25 | * relying on NextJS routing and react-router-dom 26 | * does not play well with SSR. 27 | * 28 | * While doing the transition to NextJS routing, 29 | * we need a way to support both ways of functioning. 30 | * 31 | * Please note, this is only useful in the context of 32 | * local development (`npm run dev`), as this app is 33 | * currently being built as a static export 34 | * and no rewrite is going to be actually run in production 35 | */ 36 | { 37 | source: "/:any*", 38 | destination: "/", 39 | }, 40 | ]; 41 | }, 42 | } 43 | 44 | const withTM = require("next-transpile-modules")([ 45 | "@cloudscape-design/components", 46 | ]); 47 | 48 | module.exports = withTM(nextConfig); -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/img/3P-Logos.NOTICE: -------------------------------------------------------------------------------- 1 | This product includes assets from Canonical 2 | https://design.ubuntu.com/brand/ubuntu-logo/ 3 | 4 | This product includes assets from Centos 5 | https://wiki.centos.org/ArtWork/Brand/Logo 6 | -------------------------------------------------------------------------------- /frontend/public/img/ebs.svg: -------------------------------------------------------------------------------- 1 | Amazon-Elastic-Block-Store-EBS_dark-bg -------------------------------------------------------------------------------- /frontend/public/img/ec2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/img/efs.svg: -------------------------------------------------------------------------------- 1 | Amazon-Elastic-File-System_EFS_dark-bg -------------------------------------------------------------------------------- /frontend/public/img/error_pages_illustration.svg: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /frontend/public/img/fsx.svg: -------------------------------------------------------------------------------- 1 | Amazon-FSx-for-Lustre_dark-bg -------------------------------------------------------------------------------- /frontend/public/img/od.svg: -------------------------------------------------------------------------------- 1 | Amazon-EC2_Instance_dark-bg -------------------------------------------------------------------------------- /frontend/public/img/pcluster.svg: -------------------------------------------------------------------------------- 1 | Product-Name_dark-bg -------------------------------------------------------------------------------- /frontend/public/img/queue.svg: -------------------------------------------------------------------------------- 1 | Amazon-Elastic-Container-Service_Container2_dark-bg -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /frontend/scripts/git-secrets-command.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The following script checks if git-secrets is installed on a local machine. 4 | # If not, it prints an error message pointing to the official awslabs git-secrets repository. 5 | # If yes, it executes the 'git-secrets' command with the argument(s) passed to the script. 6 | # 7 | # Usage: 8 | # get-secrets-command.sh [COMMAND] 9 | # 10 | # Examples: 11 | # $ get-secrets-command.sh '--register-aws > /dev/null' 12 | # $ get-secrets-command.sh '--pre_commit_hook -- "$@"' 13 | # 14 | 15 | if [ -z "${CI}" ] || [ "${CI}" != true ]; then 16 | if ! command -v git-secrets >/dev/null 2>&1; then 17 | echo "git-secrets is not installed. Please visit https://github.com/awslabs/git-secrets#installing-git-secrets" 18 | exit 1 19 | fi 20 | 21 | _command="git-secrets $@" 22 | eval "$_command" 23 | fi 24 | -------------------------------------------------------------------------------- /frontend/src/__tests__/DescribeCluster.test.ts: -------------------------------------------------------------------------------- 1 | import {DescribeCluster} from '../model' 2 | 3 | const mockGet = jest.fn() 4 | 5 | jest.mock('axios', () => ({ 6 | create: () => ({ 7 | get: (...args: unknown[]) => mockGet(...args), 8 | }), 9 | })) 10 | 11 | describe('given a DescribeCluster command and a cluster name', () => { 12 | const clusterName = 'any-name' 13 | 14 | describe('when the cluster can be described successfully', () => { 15 | beforeEach(() => { 16 | const mockResponse = { 17 | some: 'data', 18 | } 19 | mockGet.mockResolvedValueOnce({data: mockResponse}) 20 | }) 21 | 22 | it('should return the cluster description', async () => { 23 | const data = await DescribeCluster(clusterName) 24 | expect(data).toEqual({ 25 | some: 'data', 26 | }) 27 | }) 28 | }) 29 | 30 | describe('when the describe cluster fails', () => { 31 | let mockErrorCallback: jest.Mock 32 | let mockError: any 33 | 34 | beforeEach(() => { 35 | mockErrorCallback = jest.fn() 36 | mockError = { 37 | response: { 38 | data: { 39 | message: 'some-error-messasge', 40 | }, 41 | }, 42 | } 43 | mockGet.mockRejectedValueOnce(mockError) 44 | }) 45 | 46 | it('should call the error callback', async () => { 47 | try { 48 | await DescribeCluster(clusterName, mockErrorCallback) 49 | } catch (e) { 50 | expect(mockErrorCallback).toHaveBeenCalledTimes(1) 51 | } 52 | }) 53 | 54 | it('should re-throw the error', async () => { 55 | try { 56 | await DescribeCluster(clusterName, mockErrorCallback) 57 | } catch (e) { 58 | expect(e).toEqual({ 59 | response: { 60 | data: { 61 | message: 'some-error-messasge', 62 | }, 63 | }, 64 | }) 65 | } 66 | }) 67 | }) 68 | }) 69 | -------------------------------------------------------------------------------- /frontend/src/__tests__/console.test.tsx: -------------------------------------------------------------------------------- 1 | import {consoleDomain} from '../store' 2 | 3 | describe('Given a function to determine the console endpoint', () => { 4 | describe('when the current region is the US government one', () => { 5 | it('should point to the specific domain', () => { 6 | const domain = consoleDomain('us-gov') 7 | expect(domain).toBe('https://console.amazonaws-us-gov.com') 8 | }) 9 | }) 10 | describe('when the current region is NOT the US government one', () => { 11 | it('should point to the regional domain', () => { 12 | const domain = consoleDomain('eu-central-1') 13 | expect(domain).toBe('https://eu-central-1.console.aws.amazon.com') 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /frontend/src/__tests__/util.test.tsx: -------------------------------------------------------------------------------- 1 | import {clamp} from '../util' 2 | 3 | describe('Given a function to clamp a number using an option step discretization', () => { 4 | describe('when the input is below the minimum', () => { 5 | it('should be set to the minimum', () => { 6 | const result = clamp(1, 20, 200) 7 | expect(result).toBe(20) 8 | }) 9 | }) 10 | describe('when the input is equal to the minimum', () => { 11 | it('should be set to the minimum', () => { 12 | const result = clamp(20, 20, 200) 13 | expect(result).toBe(20) 14 | }) 15 | }) 16 | describe('when the input is above the maximum', () => { 17 | it('should be set to the maximum', () => { 18 | const result = clamp(201, 20, 200) 19 | expect(result).toBe(200) 20 | }) 21 | }) 22 | describe('when the input is equal to the maximum', () => { 23 | it('should be set to the maximum', () => { 24 | const result = clamp(200, 20, 200) 25 | expect(result).toBe(200) 26 | }) 27 | }) 28 | describe('when the input is not at the step size', () => { 29 | it('should be set to a multiple of the step size', () => { 30 | const result = clamp(21, 20, 200, 20) 31 | expect(result).toBe(20) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /frontend/src/app-config/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {AxiosInstance} from 'axios' 12 | import {mock, MockProxy} from 'jest-mock-extended' 13 | import {getAppConfig} from '..' 14 | 15 | describe('given a function to fetch the application configuration', () => { 16 | describe('and an axios instance', () => { 17 | let mockGet: jest.Mock 18 | let mockAxiosInstance: MockProxy 19 | beforeEach(() => { 20 | mockGet = jest.fn() 21 | mockAxiosInstance = mock() 22 | }) 23 | describe('when the configuration is available', () => { 24 | beforeEach(() => { 25 | const mockAppConfig = { 26 | auth_url: 'some-url', 27 | client_id: 'some-id', 28 | scopes: 'some-list', 29 | redirect_uri: 'some-uri', 30 | } 31 | mockAxiosInstance.get.mockResolvedValueOnce({data: mockAppConfig}) 32 | }) 33 | it('should map the received configuration to the known AppConfig', async () => { 34 | const config = await getAppConfig(mockAxiosInstance) 35 | expect(config).toEqual({ 36 | authUrl: 'some-url', 37 | clientId: 'some-id', 38 | redirectUri: 'some-uri', 39 | scopes: 'some-list', 40 | }) 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /frontend/src/app-config/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {AxiosInstance} from 'axios' 12 | import {AppConfig} from './types' 13 | 14 | interface RawAppConfig { 15 | auth_url: string 16 | client_id: string 17 | scopes: string 18 | redirect_uri: string 19 | } 20 | 21 | function mapAppConfig(data: RawAppConfig): AppConfig { 22 | return { 23 | authUrl: data.auth_url, 24 | clientId: data.client_id, 25 | redirectUri: data.redirect_uri, 26 | scopes: data.scopes, 27 | } 28 | } 29 | 30 | export async function getAppConfig( 31 | axiosInstance: AxiosInstance, 32 | ): Promise { 33 | const {data} = await axiosInstance.get('manager/get_app_config') 34 | return data ? mapAppConfig(data) : {} 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/app-config/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | export interface AppConfig { 12 | authUrl: string 13 | clientId: string 14 | scopes: string 15 | redirectUri: string 16 | } 17 | -------------------------------------------------------------------------------- /frontend/src/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const USER_ROLES_CLAIM = 'user_roles' 2 | -------------------------------------------------------------------------------- /frontend/src/auth/handleNotAuthorizedErrors.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {AxiosError} from 'axios' 12 | import {AppConfig} from '../app-config/types' 13 | import {generateRandomId} from '../util' 14 | 15 | export const handleNotAuthorizedErrors = 16 | ({authUrl, clientId, scopes, redirectUri}: AppConfig) => 17 | async (requestPromise: Promise) => { 18 | return requestPromise.catch(error => { 19 | switch ((error as AxiosError).response?.status) { 20 | case 401: 21 | case 403: 22 | redirectToAuthServer(authUrl, clientId, scopes, redirectUri) 23 | return Promise.reject(error) 24 | } 25 | return Promise.reject(error) 26 | }) 27 | } 28 | 29 | function redirectToAuthServer( 30 | authUrl: string, 31 | clientId: string, 32 | scopes: string, 33 | redirectUri: string, 34 | ) { 35 | const url = `${authUrl}?response_type=code&client_id=${clientId}&scope=${encodeURIComponent( 36 | scopes, 37 | )}&redirect_uri=${redirectUri}&state=${generateRandomId()}` 38 | window.location.replace(url) 39 | } 40 | -------------------------------------------------------------------------------- /frontend/src/components/ConfigView.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | // 12 | import * as React from 'react' 13 | 14 | // UI Elements 15 | import {CodeEditor} from '@cloudscape-design/components' 16 | 17 | export default function ConfigView({config, pending, onChange}: any) { 18 | const [preferences, setPreferences] = React.useState({theme: 'textmate'}) 19 | return ( 20 | {}} 25 | onDelayedChange={onChange} 26 | // @ts-expect-error TS(2322) FIXME: Type '{ theme: string; }' is not assignable to typ... Remove this comment to see the full error message 27 | preferences={preferences} 28 | onPreferencesChange={e => setPreferences(e.detail)} 29 | onValidate={e => {}} 30 | loading={pending ? true : false} 31 | i18nStrings={{ 32 | loadingState: 'Loading code editor', 33 | errorState: 'There was an error loading the code editor.', 34 | errorStateRecovery: 'Retry', 35 | editorGroupAriaLabel: 'Code editor', 36 | statusBarGroupAriaLabel: 'Status bar', 37 | cursorPosition: (row, column) => `Ln ${row}, Col ${column}`, 38 | errorsTab: 'Errors', 39 | warningsTab: 'Warnings', 40 | preferencesButtonAriaLabel: 'Preferences', 41 | paneCloseButtonAriaLabel: 'Close', 42 | preferencesModalHeader: 'Preferences', 43 | preferencesModalCancel: 'Cancel', 44 | preferencesModalConfirm: 'Confirm', 45 | preferencesModalWrapLines: 'Wrap lines', 46 | preferencesModalTheme: 'Theme', 47 | preferencesModalLightThemes: 'Light themes', 48 | preferencesModalDarkThemes: 'Dark themes', 49 | }} 50 | /> 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/components/DeleteDialog.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import * as React from 'react' 12 | 13 | // UI Elements 14 | import {Box, Button, Modal, SpaceBetween} from '@cloudscape-design/components' 15 | 16 | import {setState, useState} from '../store' 17 | import {useTranslation} from 'react-i18next' 18 | 19 | export function showDialog(id: any) { 20 | setState(['app', 'confirmDelete', id], true) 21 | } 22 | 23 | export function hideDialog(id: any) { 24 | setState(['app', 'confirmDelete', id], false) 25 | } 26 | 27 | export function DeleteDialog({children, deleteCallback, header, id}: any) { 28 | const {t} = useTranslation() 29 | const open = useState(['app', 'confirmDelete', id]) 30 | 31 | const cancel = () => { 32 | setState(['app', 'confirmDelete', id], false) 33 | } 34 | 35 | return ( 36 | 43 | 44 | 45 | 48 | 49 | 50 | } 51 | header={header} 52 | > 53 | {children} 54 | 55 | ) 56 | } 57 | 58 | // export {DeleteDialog, showDialog}; 59 | -------------------------------------------------------------------------------- /frontend/src/components/EmptyState.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | // UI Elements 13 | import {Box} from '@cloudscape-design/components' 14 | 15 | export default function EmptyState({title, subtitle, action}: any) { 16 | return ( 17 | 18 | 19 | {title} 20 | 21 | 22 | {subtitle} 23 | 24 | {action} 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/components/InfoLink.tsx: -------------------------------------------------------------------------------- 1 | import {Link} from '@cloudscape-design/components' 2 | import {ReactElement, useCallback} from 'react' 3 | import {useTranslation} from 'react-i18next' 4 | import {useHelpPanel} from './help-panel/HelpPanel' 5 | 6 | type InfoLinkProps = { 7 | helpPanel: ReactElement 8 | ariaLabel?: string 9 | } 10 | 11 | function InfoLink({ariaLabel, helpPanel}: InfoLinkProps) { 12 | const {updateHelpPanel} = useHelpPanel() 13 | const {t} = useTranslation() 14 | 15 | const setHelpPanel = useCallback(() => { 16 | updateHelpPanel({element: helpPanel, open: true}) 17 | }, [updateHelpPanel, helpPanel]) 18 | 19 | return ( 20 | 25 | {t('infoLink.label')} 26 | 27 | ) 28 | } 29 | 30 | export default InfoLink 31 | -------------------------------------------------------------------------------- /frontend/src/components/InputErrors.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import {SpaceBetween} from '@cloudscape-design/components' 12 | 13 | // UI Elements 14 | export default function InputErrors({errors}: any) { 15 | return ( 16 | errors && ( 17 |
18 | 19 | {errors.map((error: any, i: any) => ( 20 |
21 | * {error} 22 |
23 | ))} 24 |
25 |
26 | ) 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/components/Loading.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import Spinner from '@cloudscape-design/components/spinner' 12 | import {useTranslation} from 'react-i18next' 13 | 14 | export default function Loading(props: any) { 15 | const {t} = useTranslation() 16 | const defaultText = t('components.Loading.text') 17 | return ( 18 |
27 | 28 | 29 | {' '} 30 | {props.text || defaultText} 31 | 32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/components/NoMatch.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | import { 13 | Container, 14 | ContentLayout, 15 | Link, 16 | TextContent, 17 | } from '@cloudscape-design/components' 18 | import React from 'react' 19 | import errorPage from './../../public/img/error_pages_illustration.svg' 20 | import Image from 'next/image' 21 | import {useTranslation} from 'react-i18next' 22 | import Layout from '../old-pages/Layout' 23 | import {DefaultHelpPanel} from './help-panel/DefaultHelpPanel' 24 | import {useHelpPanel} from './help-panel/HelpPanel' 25 | 26 | export function NoMatch() { 27 | const {t} = useTranslation() 28 | useHelpPanel() 29 | 30 | return ( 31 | 32 | }> 33 | 34 | 35 | 36 |

{t('noMatch.title')}

37 |

{t('noMatch.description')}

38 |

{t('noMatch.links')}

39 |
    40 |
  • 41 | {t('noMatch.home')} 42 |
  • 43 |
  • 44 | 49 | {t('global.docs.title')} 50 | 51 |
  • 52 |
53 |
54 |
55 |
56 |
57 | ) 58 | } 59 | -------------------------------------------------------------------------------- /frontend/src/components/ValueWithLabel.tsx: -------------------------------------------------------------------------------- 1 | import {SpaceBetween, Box} from '@cloudscape-design/components' 2 | import {PropsWithChildren, ReactElement} from 'react' 3 | 4 | export const ValueWithLabel = ({ 5 | label, 6 | children, 7 | info, 8 | }: PropsWithChildren<{label: string; info?: ReactElement}>) => ( 9 |
10 | 11 | 16 | {label} 17 | 18 | {info} 19 | 20 |
{children}
21 |
22 | ) 23 | -------------------------------------------------------------------------------- /frontend/src/components/__tests__/useClusterPoll.test.tsx: -------------------------------------------------------------------------------- 1 | import {renderHook} from '@testing-library/react-hooks' 2 | import {useClusterPoll} from '../useClusterPoll' 3 | 4 | jest.mock('../../model', () => ({ 5 | DescribeCluster: jest.fn(), 6 | })) 7 | import {DescribeCluster} from '../../model' 8 | import {act} from 'react-dom/test-utils' 9 | 10 | describe('Given a cluster poll', () => { 11 | beforeEach(() => jest.useFakeTimers()) 12 | afterEach(() => { 13 | jest.resetAllMocks() 14 | jest.useRealTimers() 15 | }) 16 | 17 | describe('when a cluster name is given', () => { 18 | it('should start polling the resource', () => { 19 | renderHook(() => useClusterPoll('Test', true)) 20 | 21 | jest.advanceTimersByTime(6000) 22 | expect(DescribeCluster).toHaveBeenCalledWith('Test') 23 | }) 24 | 25 | it('can start polling the resource on demand', () => { 26 | const {result} = renderHook(() => useClusterPoll('Test', false)) 27 | jest.advanceTimersByTime(6000) 28 | expect(DescribeCluster).not.toHaveBeenCalled() 29 | act(() => { 30 | result.current.start() 31 | }) 32 | jest.advanceTimersByTime(6000) 33 | 34 | expect(DescribeCluster).toHaveBeenCalledWith('Test') 35 | }) 36 | 37 | it('can stop polling after it has been started', () => { 38 | const {result} = renderHook(() => useClusterPoll('Test', false)) 39 | act(() => { 40 | result.current.start() 41 | result.current.stop() 42 | }) 43 | jest.advanceTimersByTime(6000) 44 | 45 | expect(DescribeCluster).not.toHaveBeenCalled() 46 | }) 47 | }) 48 | 49 | describe('when a cluster name is not given', () => { 50 | it('should not poll any resource', () => { 51 | renderHook(() => useClusterPoll('', true)) 52 | jest.advanceTimersByTime(6000) 53 | 54 | expect(DescribeCluster).not.toHaveBeenCalled() 55 | }) 56 | }) 57 | }) 58 | -------------------------------------------------------------------------------- /frontend/src/components/date/DateView.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import React from 'react' 12 | import AbsoluteTimestamp, {TimeZone} from './AbsoluteTimestamp' 13 | 14 | type DateViewProps = { 15 | date: string 16 | locales?: string | string[] 17 | timeZone?: TimeZone 18 | } 19 | 20 | export default function DateView({ 21 | date, 22 | locales = 'en-Us', 23 | timeZone = TimeZone.Local, 24 | }: DateViewProps) { 25 | const timestamp = Date.parse(date) 26 | return ( 27 | 28 | {timestamp} 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/components/date/__tests__/DateView.test.tsx: -------------------------------------------------------------------------------- 1 | import {render, RenderResult, waitFor} from '@testing-library/react' 2 | import {TimeZone} from '../AbsoluteTimestamp' 3 | import DateView from '../DateView' 4 | import tzmock from 'timezone-mock' 5 | 6 | describe('Given a DateView component', () => { 7 | tzmock.register('UTC') 8 | let renderResult: RenderResult 9 | 10 | describe('when only a string date is provided', () => { 11 | const dateString = '2022-09-16T14:03:35.000Z' 12 | const expectedResult = 'September 16, 2022 at 14:03 (UTC)' 13 | 14 | beforeEach(async () => { 15 | renderResult = await waitFor(() => render()) 16 | }) 17 | 18 | it('should render the date in the expected absolute format with default locales and timezone', async () => { 19 | expect(renderResult.getByText(expectedResult)).toBeTruthy() 20 | }) 21 | }) 22 | 23 | describe('when a date string, a locale and a timezone is provided', () => { 24 | const dateString = '2022-09-16T14:03:35.000Z' 25 | const locale = 'it-IT' 26 | const timeZone = TimeZone.UTC 27 | const expectedResult = '16 settembre 2022 14:03 (UTC)' 28 | 29 | beforeEach(async () => { 30 | renderResult = await waitFor(() => 31 | render( 32 | , 33 | ), 34 | ) 35 | }) 36 | 37 | it('should render the date in the expeted absolute format with provided locales and timezone', async () => { 38 | expect(renderResult.getByText(expectedResult)).toBeTruthy() 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/src/components/help-panel/DefaultHelpPanel.tsx: -------------------------------------------------------------------------------- 1 | import {useTranslation} from 'react-i18next' 2 | import TitleDescriptionHelpPanel from './TitleDescriptionHelpPanel' 3 | 4 | export const DefaultHelpPanel = () => { 5 | const {t} = useTranslation() 6 | return ( 7 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /frontend/src/components/help-panel/HelpPanel.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, 3 | Dispatch, 4 | FunctionComponent, 5 | ReactElement, 6 | SetStateAction, 7 | useCallback, 8 | useContext, 9 | useEffect, 10 | useState, 11 | } from 'react' 12 | 13 | type PanelContext = { 14 | element: ReactElement 15 | open: boolean 16 | } 17 | 18 | const DEFAULT_PANEL_CONTEXT = { 19 | element: <>, 20 | open: false, 21 | } 22 | 23 | const HelpPanelContext = createContext< 24 | [PanelContext, Dispatch>] 25 | >([DEFAULT_PANEL_CONTEXT, () => null]) 26 | 27 | export const HelpPanelProvider: FunctionComponent = ({children}) => { 28 | const helpPanelState = useState(DEFAULT_PANEL_CONTEXT) 29 | return ( 30 | 31 | {children} 32 | 33 | ) 34 | } 35 | 36 | /* 37 | * Hook used to update and show Cloudscape help system 38 | * https://cloudscape.design/patterns/general/help-system/ 39 | * 40 | * Examples: 41 | * 42 | * Show the help panel when loading a page 43 | * ``` 44 | * useHelpPanel() 45 | * ``` 46 | * 47 | * Update and show the help panel when tapping on an Info link 48 | * ``` 49 | * const { updateHelpPanel } = useHelpPanel() 50 | * updateHelpPanel({ element: , open: true}) 51 | * ``` 52 | */ 53 | 54 | export const useHelpPanel = (initialPanel?: ReactElement) => { 55 | const [helpPanel, setHelpPanel] = useContext(HelpPanelContext) 56 | 57 | const updateHelpPanel = useCallback( 58 | ({element, open}: {element?: ReactElement; open?: boolean}) => { 59 | setHelpPanel({ 60 | element: element || helpPanel.element, 61 | open: typeof open !== 'undefined' ? open : helpPanel.open, 62 | }) 63 | }, 64 | [setHelpPanel, helpPanel], 65 | ) 66 | 67 | // This useEffect simplify the usage of the component 68 | // when displaying the help panel at page load 69 | // and avoid placing effects in the main page component 70 | useEffect(() => { 71 | if (initialPanel) { 72 | updateHelpPanel({element: initialPanel}) 73 | } 74 | // eslint-disable-next-line react-hooks/exhaustive-deps 75 | }, []) 76 | 77 | return {...helpPanel, updateHelpPanel} 78 | } 79 | -------------------------------------------------------------------------------- /frontend/src/components/help-panel/TitleDescriptionHelpPanel.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | import {HelpPanel, Icon, Link} from '@cloudscape-design/components' 13 | import {ReactElement, useMemo} from 'react' 14 | import {useTranslation} from 'react-i18next' 15 | 16 | interface TitleDescriptionHelpPanelProps { 17 | title: string | ReactElement 18 | description: string | ReactElement 19 | footerLinks?: {title: string; href: string}[] 20 | } 21 | 22 | function TitleDescriptionHelpPanel({ 23 | title, 24 | description, 25 | footerLinks, 26 | }: TitleDescriptionHelpPanelProps) { 27 | const {t} = useTranslation() 28 | 29 | const DEFAULT_FOOTER_LINKS = useMemo( 30 | () => [ 31 | { 32 | title: t('global.docs.title'), 33 | href: t('global.docs.link'), 34 | }, 35 | ], 36 | [t], 37 | ) 38 | 39 | return ( 40 | {title}} 42 | footer={ 43 |
44 |

45 | {t('helpPanel.footer.learnMore')} 46 |

47 |
    48 | {(footerLinks || DEFAULT_FOOTER_LINKS).map(link => ( 49 |
  • 50 | 55 | {link.title} 56 | 57 |
  • 58 | ))} 59 |
60 |
61 | } 62 | > 63 |
{description}
64 |
65 | ) 66 | } 67 | 68 | export default TitleDescriptionHelpPanel 69 | -------------------------------------------------------------------------------- /frontend/src/components/useClusterPoll.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect, useState} from 'react' 2 | import {DescribeCluster} from '../model' 3 | 4 | export const useClusterPoll = (clusterName: string, startOnRender: boolean) => { 5 | const [start, setStart] = useState(startOnRender) 6 | 7 | useEffect(() => { 8 | if (!start) return 9 | const timerId = setInterval( 10 | () => clusterName && DescribeCluster(clusterName), 11 | 5000, 12 | ) 13 | return () => clearInterval(timerId) 14 | }, [start, clusterName]) 15 | 16 | return { 17 | start: () => setStart(true), 18 | stop: () => setStart(false), 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/src/feature-flags/__tests__/useFeatureFlag.test.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {renderHook} from '@testing-library/react-hooks' 12 | import {useFeatureFlag} from '../useFeatureFlag' 13 | 14 | const mockUseState = jest.fn() 15 | 16 | jest.mock('../../store', () => ({ 17 | ...(jest.requireActual('../../store') as any), 18 | useState: (...args: unknown[]) => mockUseState(...args), 19 | })) 20 | 21 | describe('given a hook to test whether a feature flag is active', () => { 22 | describe('when the feature is active', () => { 23 | beforeEach(() => { 24 | mockUseState.mockReturnValue('3.2.0') 25 | }) 26 | 27 | it('should return true', () => { 28 | const {result} = renderHook(() => useFeatureFlag('fsx_ontap')) 29 | expect(result.current).toBe(true) 30 | }) 31 | }) 32 | 33 | describe('when the feature is not active', () => { 34 | beforeEach(() => { 35 | mockUseState.mockReturnValue('3.1.0') 36 | }) 37 | 38 | it('should return false', () => { 39 | const {result} = renderHook(() => useFeatureFlag('fsx_ontap')) 40 | expect(result.current).toBe(false) 41 | }) 42 | }) 43 | }) 44 | -------------------------------------------------------------------------------- /frontend/src/feature-flags/featureFlagsProvider.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {AvailableFeature} from './types' 12 | 13 | const versionToFeaturesMap: Record = { 14 | '3.1.0': ['multiuser_cluster'], 15 | '3.2.0': [ 16 | 'fsx_ontap', 17 | 'fsx_openzsf', 18 | 'lustre_persistent2', 19 | 'memory_based_scheduling', 20 | 'multiuser_cluster', 21 | 'slurm_queue_update_strategy', 22 | ], 23 | '3.3.0': [ 24 | 'slurm_accounting', 25 | 'queues_multiple_instance_types', 26 | 'dynamic_fs_mount', 27 | ], 28 | '3.4.0': ['multi_az', 'on_node_updated'], 29 | } 30 | 31 | function composeFlagsListByVersion(currentVersion: string): AvailableFeature[] { 32 | let features: Set = new Set([]) 33 | 34 | for (let version in versionToFeaturesMap) { 35 | if (currentVersion >= version) { 36 | features = new Set([...features, ...versionToFeaturesMap[version]]) 37 | } 38 | } 39 | 40 | return Array.from(features) 41 | } 42 | 43 | export function featureFlagsProvider(version: string): AvailableFeature[] { 44 | const features: AvailableFeature[] = [] 45 | 46 | return features.concat(composeFlagsListByVersion(version)) 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/feature-flags/types.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | export type AvailableFeature = 12 | | 'fsx_ontap' 13 | | 'fsx_openzsf' 14 | | 'lustre_persistent2' 15 | | 'multiuser_cluster' 16 | | 'memory_based_scheduling' 17 | | 'slurm_accounting' 18 | | 'slurm_queue_update_strategy' 19 | | 'queues_multiple_instance_types' 20 | | 'multi_az' 21 | | 'on_node_updated' 22 | | 'dynamic_fs_mount' 23 | -------------------------------------------------------------------------------- /frontend/src/feature-flags/useFeatureFlag.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {useState} from '../store' 12 | import {featureFlagsProvider} from './featureFlagsProvider' 13 | import {AvailableFeature} from './types' 14 | 15 | export function useFeatureFlag(feature: AvailableFeature): boolean { 16 | const version = useState(['app', 'version', 'full']) 17 | return isFeatureEnabled(version, feature) 18 | } 19 | 20 | export function isFeatureEnabled( 21 | version: string, 22 | feature: AvailableFeature, 23 | ): boolean { 24 | const features = new Set(featureFlagsProvider(version)) 25 | 26 | return features.has(feature) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/http/csrf.ts: -------------------------------------------------------------------------------- 1 | import {RequestParams} from './executeRequest' 2 | 3 | export const requestWithCSRF = async ( 4 | internalRequest: (...params: RequestParams) => Promise, 5 | ...params: RequestParams 6 | ) => { 7 | const [method, url, body, headers, appConfig] = params 8 | 9 | if (method === 'get') { 10 | return internalRequest(...params) 11 | } 12 | const {data} = (await internalRequest( 13 | 'get', 14 | '/csrf', 15 | null, 16 | {}, 17 | appConfig, 18 | )) as { 19 | data: {csrf_token: string} 20 | } 21 | const tokenHeader = { 22 | 'X-CSRF-Token': data.csrf_token, 23 | } 24 | return internalRequest( 25 | method, 26 | url, 27 | body, 28 | {...headers, ...tokenHeader}, 29 | appConfig, 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /frontend/src/http/executeRequest.tsx: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import {AppConfig} from '../app-config/types' 3 | import {handleNotAuthorizedErrors} from '../auth/handleNotAuthorizedErrors' 4 | import identityFn from 'lodash/identity' 5 | import {requestWithCSRF} from './csrf' 6 | 7 | export const axiosInstance = axios.create({ 8 | baseURL: getHost(), 9 | }) 10 | 11 | function getHost() { 12 | if (process.env.NODE_ENV !== 'production') return 'http://localhost:5001/' 13 | return '/' 14 | } 15 | 16 | export type HTTPMethod = 'get' | 'put' | 'post' | 'patch' | 'delete' 17 | 18 | export type RequestParams = [ 19 | method: HTTPMethod, 20 | url: string, 21 | body?: any, 22 | headers?: Record, 23 | appConfig?: AppConfig, 24 | ] 25 | 26 | export function internalExecuteRequest(...params: RequestParams) { 27 | const [method, url, body, headers, appConfig] = params 28 | const requestFunc = axiosInstance[method] 29 | 30 | const headersToSend = {'Content-Type': 'application/json', ...headers} 31 | const handle401and403 = appConfig 32 | ? handleNotAuthorizedErrors(appConfig) 33 | : identityFn> 34 | 35 | const promise = 36 | method === 'get' || method === 'delete' 37 | ? requestFunc(url, {headers: headersToSend}) 38 | : requestFunc(url, body, {headers: headersToSend}) 39 | 40 | return handle401and403(promise) 41 | } 42 | 43 | export const executeRequest = (...params: RequestParams) => 44 | requestWithCSRF(internalExecuteRequest, ...params) 45 | -------------------------------------------------------------------------------- /frontend/src/http/httpLogs.ts: -------------------------------------------------------------------------------- 1 | import {AxiosError, AxiosInstance} from 'axios' 2 | import {ILogger} from '../logger/ILogger' 3 | 4 | export const enableHttpLogs = ( 5 | axiosInstance: AxiosInstance, 6 | logger: ILogger, 7 | ) => { 8 | const requestInterceptor = axiosInstance.interceptors.request.use(config => { 9 | if (!isLogEndpoint(config.url)) { 10 | logger.info('HTTP request started', { 11 | url: config.url, 12 | }) 13 | } 14 | return config 15 | }) 16 | const responseInterceptor = axiosInstance.interceptors.response.use( 17 | response => { 18 | if (!isLogEndpoint(response.config?.url)) { 19 | logger.info('HTTP response received', { 20 | url: response.config?.url, 21 | statusCode: response.status, 22 | }) 23 | } 24 | return response 25 | }, 26 | (error: AxiosError) => { 27 | if (!isLogEndpoint(error.config?.url)) { 28 | logger.error('HTTP response received', { 29 | url: error.config?.url, 30 | statusCode: error.response?.status, 31 | }) 32 | } 33 | throw error 34 | }, 35 | ) 36 | return () => { 37 | axiosInstance.interceptors.request.eject(requestInterceptor) 38 | axiosInstance.interceptors.response.eject(responseInterceptor) 39 | } 40 | } 41 | 42 | const isLogEndpoint = (url: string | undefined) => 43 | url && url.indexOf('/logs') > -1 44 | -------------------------------------------------------------------------------- /frontend/src/i18n-resources.d.ts: -------------------------------------------------------------------------------- 1 | // src/i18n-resources.d.ts 2 | 3 | import 'react-i18next' 4 | 5 | declare module 'react-i18next' { 6 | export interface Resources { 7 | translation: typeof import('../locales/en/strings.json') 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /frontend/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import {initReactI18next} from 'react-i18next' 3 | import enStrings from '../../locales/en/strings.json' 4 | 5 | const resources = { 6 | en: { 7 | translation: enStrings, 8 | }, 9 | } 10 | 11 | i18n.use(initReactI18next).init({ 12 | resources, 13 | lng: 'en', // language to use, more information here: https://www.i18next.com/overview/configuration-options#languages-namespaces-resources 14 | // you can use the i18n.changeLanguage function to change the language manually: https://www.i18next.com/overview/api#changelanguage 15 | // if you're using a language detector, do not define the lng option 16 | interpolation: { 17 | escapeValue: false, // react already safes from xss 18 | }, 19 | }) 20 | 21 | export default i18n 22 | -------------------------------------------------------------------------------- /frontend/src/logger/ConsoleLogger.ts: -------------------------------------------------------------------------------- 1 | import {ILogger} from './ILogger' 2 | 3 | export class ConsoleLogger implements ILogger { 4 | critical( 5 | message: Error | string, 6 | extra?: Record, 7 | source?: string, 8 | ): void { 9 | console.error(this.formatMessage(message, extra, source)) 10 | } 11 | 12 | debug( 13 | message: string, 14 | extra?: Record, 15 | source?: string, 16 | ): void { 17 | console.debug(this.formatMessage(message, extra, source)) 18 | } 19 | 20 | error( 21 | message: Error | string, 22 | extra?: Record, 23 | source?: string, 24 | ): void { 25 | console.error(this.formatMessage(message, extra, source)) 26 | } 27 | 28 | info( 29 | message: string, 30 | extra?: Record, 31 | source?: string, 32 | ): void { 33 | console.info(this.formatMessage(message, extra, source)) 34 | } 35 | 36 | warning( 37 | message: string, 38 | extra?: Record, 39 | source?: string, 40 | ): void { 41 | console.warn(this.formatMessage(message, extra, source)) 42 | } 43 | 44 | private formatMessage( 45 | message: Error | string, 46 | extra: Record | undefined, 47 | source?: string, 48 | ) { 49 | if (!extra) extra = {} 50 | 51 | extra['source'] ||= source 52 | return `${message}, extra: ${JSON.stringify(extra)}` 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/src/logger/ILogger.ts: -------------------------------------------------------------------------------- 1 | export interface ILogger { 2 | info(message: string, extra?: Record, source?: string): void 3 | 4 | warning( 5 | message: string, 6 | extra?: Record, 7 | source?: string, 8 | ): void 9 | 10 | debug(message: string, extra?: Record, source?: string): void 11 | 12 | error( 13 | message: Error | string, 14 | extra?: Record, 15 | source?: string, 16 | ): void 17 | 18 | critical( 19 | message: Error | string, 20 | extra?: Record, 21 | source?: string, 22 | ): void 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/logger/LoggerProvider.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react' 2 | import {ILogger} from './ILogger' 3 | import {executeRequest} from '../http/executeRequest' 4 | import {ConsoleLogger} from './ConsoleLogger' 5 | import {Logger} from './RemoteLogger' 6 | 7 | export const logger: ILogger = 8 | process.env.NODE_ENV !== 'production' 9 | ? new ConsoleLogger() 10 | : new Logger(executeRequest) 11 | 12 | const LoggerContext = React.createContext(logger) 13 | 14 | export function useLogger(): ILogger { 15 | return useContext(LoggerContext) 16 | } 17 | 18 | export function LoggerProvider(props: any) { 19 | return ( 20 | 21 | {props.children} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/navigation/useLocationChangeLog.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {useEffect} from 'react' 12 | import {useLocation} from 'react-router-dom' 13 | import {useLogger} from '../logger/LoggerProvider' 14 | 15 | export function useLocationChangeLog() { 16 | const logger = useLogger() 17 | const location = useLocation() 18 | 19 | useEffect(() => { 20 | logger.info('Location changed', { 21 | to: location.pathname, 22 | }) 23 | }, [location, logger]) 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/navigation/useWizardSectionChangeLog.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {useEffect} from 'react' 12 | import {useLogger} from '../logger/LoggerProvider' 13 | import {useState} from '../store' 14 | 15 | export function useWizardSectionChangeLog() { 16 | const page = useState(['app', 'wizard', 'page']) 17 | const logger = useLogger() 18 | 19 | useEffect(() => { 20 | if (!page) return 21 | 22 | logger.info('Wizard section changed:', { 23 | to: page, 24 | }) 25 | }, [logger, page]) 26 | } 27 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/StopDialog.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import * as React from 'react' 12 | 13 | // UI Elements 14 | import {Box, Button, Modal, SpaceBetween} from '@cloudscape-design/components' 15 | 16 | import {UpdateComputeFleet} from '../../model' 17 | import {setState, useState} from '../../store' 18 | 19 | function StopDialog({clusterName}: any) { 20 | const open = useState(['app', 'clusters', 'clusterStop', 'dialog']) 21 | 22 | const cancel = () => { 23 | setState(['app', 'clusters', 'clusterStop', 'dialog'], false) 24 | } 25 | 26 | const stopCluster = () => { 27 | UpdateComputeFleet(clusterName, 'STOP_REQUESTED') 28 | cancel() 29 | } 30 | 31 | return ( 32 | 39 | 40 | 41 | 42 | 43 | 44 | } 45 | header="Stop Cluster?" 46 | > 47 | Are you sure you wish to stop cluster {clusterName}? This will terminate 48 | all jobs that are running and terminate any compute nodes running. 49 | 50 | ) 51 | } 52 | 53 | function stopComputeFleet() { 54 | setState(['app', 'clusters', 'clusterStop', 'dialog'], true) 55 | } 56 | 57 | export {StopDialog, stopComputeFleet} 58 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/__tests__/Filesystems.test.ts: -------------------------------------------------------------------------------- 1 | import {mock, MockProxy} from 'jest-mock-extended' 2 | import {EC2Instance} from '../../../types/instances' 3 | import {Storages} from '../../Configure/Storage.types' 4 | import {buildFilesystemLink} from '../Filesystems' 5 | 6 | describe('given a function to build the link to the filesystem in the AWS console', () => { 7 | let mockHeadNode: MockProxy 8 | const mockFileSystem = mock({MountDir: 'some-mount-dir'}) 9 | const mockRegion = 'some-region' 10 | 11 | describe('when the headnode configuration is available', () => { 12 | beforeEach(() => { 13 | mockHeadNode = mock({instanceId: 'some-instance-id'}) 14 | }) 15 | 16 | it('should return the link to the filsystem', () => { 17 | expect( 18 | buildFilesystemLink(mockRegion, mockHeadNode, mockFileSystem), 19 | ).toBe( 20 | 'https://some-region.console.aws.amazon.com/systems-manager/managed-instances/some-instance-id/file-system?region=some-region&osplatform=Linux#%7B%22path%22%3A%22some-mount-dir%22%7D', 21 | ) 22 | }) 23 | }) 24 | 25 | describe('when the headnode configuration is not available', () => { 26 | beforeEach(() => { 27 | mockHeadNode = undefined 28 | }) 29 | 30 | it('should return null', () => { 31 | expect( 32 | buildFilesystemLink(mockRegion, mockHeadNode, mockFileSystem), 33 | ).toBe(null) 34 | }) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/SubnetMultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import {Multiselect, MultiselectProps} from '@cloudscape-design/components' 2 | import {NonCancelableEventHandler} from '@cloudscape-design/components/internal/events' 3 | import React from 'react' 4 | import {useMemo} from 'react' 5 | import {useState} from '../../../store' 6 | import {subnetName} from '../util' 7 | import {Subnet} from './queues.types' 8 | 9 | type SubnetMultiSelectProps = { 10 | value: string[] 11 | onChange: NonCancelableEventHandler 12 | } 13 | 14 | function SubnetMultiSelect({value, onChange}: SubnetMultiSelectProps) { 15 | const vpc = useState(['app', 'wizard', 'vpc']) 16 | const subnets = useState(['aws', 'subnets']) 17 | const filteredSubnets: Subnet[] = useMemo( 18 | () => 19 | subnets && 20 | subnets.filter((s: Subnet) => { 21 | return vpc ? s.VpcId === vpc : true 22 | }), 23 | [subnets, vpc], 24 | ) 25 | 26 | const subnetOptions = useMemo(() => { 27 | return filteredSubnets.map((subnet: Subnet) => { 28 | return { 29 | value: subnet.SubnetId, 30 | label: subnet.SubnetId, 31 | description: 32 | subnet.AvailabilityZone + 33 | ` - ${subnet.AvailabilityZoneId}` + 34 | (subnetName(subnet) ? ` (${subnetName(subnet)})` : ''), 35 | } 36 | }) 37 | }, [filteredSubnets]) 38 | 39 | return ( 40 | { 42 | return value.includes(option.value) 43 | })} 44 | onChange={onChange} 45 | options={subnetOptions} 46 | /> 47 | ) 48 | } 49 | 50 | export {SubnetMultiSelect} 51 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/__tests__/multiAZ.test.tsx: -------------------------------------------------------------------------------- 1 | import {areMultiAZSelected} from '../Queues' 2 | 3 | describe('Given a list of subnets', () => { 4 | describe('when zero subnets are selected', () => { 5 | it('should keep EFA and Placement groups enabled', () => { 6 | const {multiAZ, canUseEFA, canUsePlacementGroup} = areMultiAZSelected([]) 7 | expect(multiAZ).toBe(false) 8 | expect(canUseEFA).toBe(true) 9 | expect(canUsePlacementGroup).toBe(true) 10 | }) 11 | }) 12 | 13 | describe('when a single subnet is selected', () => { 14 | it('should keep EFA and Placement groups enabled', () => { 15 | const {multiAZ, canUseEFA, canUsePlacementGroup} = areMultiAZSelected([ 16 | 'subnet-a', 17 | ]) 18 | expect(multiAZ).toBe(false) 19 | expect(canUseEFA).toBe(true) 20 | expect(canUsePlacementGroup).toBe(true) 21 | }) 22 | }) 23 | 24 | describe('when more than one subnet is selected', () => { 25 | it('should keep EFA and Placement groups disabled', () => { 26 | const {multiAZ, canUseEFA, canUsePlacementGroup} = areMultiAZSelected([ 27 | 'subnet-a', 28 | 'subnet-b', 29 | ]) 30 | expect(multiAZ).toBe(true) 31 | expect(canUseEFA).toBe(false) 32 | expect(canUsePlacementGroup).toBe(false) 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/queues.mapper.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | 11 | import {isFeatureEnabled} from '../../../feature-flags/useFeatureFlag' 12 | import { 13 | SingleInstanceComputeResource, 14 | MultiInstanceComputeResource, 15 | } from './queues.types' 16 | 17 | function mapComputeResource( 18 | computeResource: SingleInstanceComputeResource | MultiInstanceComputeResource, 19 | ): MultiInstanceComputeResource { 20 | if ('Instances' in computeResource) { 21 | return computeResource 22 | } 23 | 24 | const {InstanceType, ...otherComputeResourceConfig} = computeResource 25 | 26 | return { 27 | ...otherComputeResourceConfig, 28 | Instances: [ 29 | { 30 | InstanceType, 31 | }, 32 | ], 33 | } 34 | } 35 | 36 | export function mapComputeResources( 37 | version: string, 38 | computeResourcesConfig: 39 | | SingleInstanceComputeResource[] 40 | | MultiInstanceComputeResource[], 41 | ) { 42 | if (!isFeatureEnabled(version, 'queues_multiple_instance_types')) { 43 | return computeResourcesConfig 44 | } 45 | 46 | return computeResourcesConfig.map(mapComputeResource) 47 | } 48 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/queues.types.ts: -------------------------------------------------------------------------------- 1 | export type Queue = { 2 | Name: string 3 | AllocationStrategy: AllocationStrategy 4 | ComputeResources: MultiInstanceComputeResource[] 5 | } 6 | 7 | export type QueueValidationErrors = Record< 8 | number, 9 | 'instance_type_unique' | 'instance_types_empty' 10 | > 11 | 12 | export type ComputeResource = { 13 | Name: string 14 | MinCount: number 15 | MaxCount: number 16 | } 17 | 18 | export type SingleInstanceComputeResource = ComputeResource & { 19 | InstanceType: string 20 | } 21 | 22 | export type AllocationStrategy = 'lowest-price' | 'capacity-optimized' 23 | 24 | export type ComputeResourceInstance = {InstanceType: string} 25 | 26 | export type MultiInstanceComputeResource = ComputeResource & { 27 | Instances: ComputeResourceInstance[] 28 | } 29 | 30 | export type Tag = { 31 | Key: string 32 | Value: string 33 | } 34 | 35 | export type Subnet = { 36 | SubnetId: string 37 | AvailabilityZone: string 38 | AvailabilityZoneId: string 39 | VpcId: string 40 | Tags?: Tag[] 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Storage.types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Used to configure the Storage part of the Cluster wizard: 3 | * - controls whether a storage type can be created or just linked 4 | * - specify if a storage type can be mounted as a file system or just one of its volumes 5 | */ 6 | export const STORAGE_TYPE_PROPS = { 7 | FsxLustre: { 8 | mountFilesystem: true, 9 | maxToCreate: 1, 10 | maxExistingToAttach: 20, 11 | }, 12 | FsxOntap: { 13 | mountFilesystem: false, 14 | maxToCreate: 0, 15 | maxExistingToAttach: 20, 16 | }, 17 | FsxOpenZfs: { 18 | mountFilesystem: false, 19 | maxToCreate: 0, 20 | maxExistingToAttach: 20, 21 | }, 22 | Efs: { 23 | mountFilesystem: true, 24 | maxToCreate: 1, 25 | maxExistingToAttach: 20, 26 | }, 27 | Ebs: { 28 | mountFilesystem: false, 29 | maxToCreate: 5, 30 | maxExistingToAttach: 5, 31 | }, 32 | } 33 | 34 | export type StorageType = keyof typeof STORAGE_TYPE_PROPS 35 | 36 | export type Storages = { 37 | Name: string 38 | StorageType: StorageType 39 | MountDir: string 40 | }[] 41 | 42 | export type UIStorageSettings = { 43 | useExisting: boolean 44 | }[] 45 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/__tests__/itemToOption.test.ts: -------------------------------------------------------------------------------- 1 | import {describe} from '@jest/globals' 2 | import {itemToOption} from '../Cluster' 3 | 4 | describe('Given a selection item', () => { 5 | describe('when the item is a string', () => { 6 | it('should return a valid SelectProps.Option', () => { 7 | const item = 'option-value' 8 | const expectedOption = {label: 'option-value', value: 'option-value'} 9 | 10 | const option = itemToOption(item) 11 | 12 | expect(option).toEqual(expectedOption) 13 | }) 14 | }) 15 | 16 | describe('when the item is a tuple of 2 strings', () => { 17 | it('should return a valid SelectProps.Option', () => { 18 | const item = ['option-value', 'option-label'] as [string, string] 19 | const expectedOption = {label: 'option-label', value: 'option-value'} 20 | 21 | const option = itemToOption(item) 22 | 23 | expect(option).toEqual(expectedOption) 24 | }) 25 | }) 26 | 27 | describe('when the item is null', () => { 28 | const item = null 29 | 30 | it('should return null', () => { 31 | const option = itemToOption(item) 32 | expect(option).toBeNull() 33 | }) 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/useWizardNavigation.ts: -------------------------------------------------------------------------------- 1 | import {getState, setState, updateState, useState} from '../../store' 2 | // @ts-expect-error TS(7016) FIXME: Could not find a declaration file for module 'js-y... Remove this comment to see the full error message 3 | import jsyaml from 'js-yaml' 4 | 5 | const pages = ['source', 'cluster', 'headNode', 'storage', 'queues', 'create'] 6 | 7 | export const useWizardNavigation = (validate: (page: string) => boolean) => { 8 | const currentPage = useState(['app', 'wizard', 'page']) || 'source' 9 | 10 | return (reason: string, requestedStepIndex: number) => { 11 | switch (reason) { 12 | case 'next': 13 | handleNext(currentPage, validate) 14 | break 15 | case 'previous': 16 | handlePrev(currentPage) 17 | break 18 | case 'step': 19 | handleStep(currentPage, pages[requestedStepIndex], validate) 20 | break 21 | default: 22 | setPage('source') 23 | } 24 | } 25 | } 26 | 27 | const handleNext = ( 28 | currentPage: string, 29 | validate: (page: string) => boolean, 30 | ) => { 31 | if (validate(currentPage)) 32 | updateState(['app', 'wizard', 'validated'], (existing: any) => 33 | (existing || new Set()).add(currentPage), 34 | ) 35 | else return 36 | 37 | const nextPage = pages[pages.indexOf(currentPage) + 1] 38 | setPage(nextPage) 39 | } 40 | 41 | const handlePrev = (currentPage: string) => { 42 | setState(['app', 'wizard', 'errors'], null) 43 | 44 | const prevPage = pages[pages.indexOf(currentPage) - 1] 45 | setPage(prevPage) 46 | } 47 | 48 | const handleStep = ( 49 | currentPage: string, 50 | requestedPage: string, 51 | validate: (page: string) => boolean, 52 | ) => { 53 | if ( 54 | pages.indexOf(requestedPage) < pages.indexOf(currentPage) || 55 | validate(currentPage) 56 | ) { 57 | setPage(requestedPage) 58 | } 59 | } 60 | 61 | function setPage(page: string) { 62 | const config = getState(['app', 'wizard', 'config']) 63 | if (page === 'create') { 64 | console.log(jsyaml.dump(config)) 65 | setState(['app', 'wizard', 'clusterConfigYaml'], jsyaml.dump(config)) 66 | } 67 | setState(['app', 'wizard', 'page'], page) 68 | } 69 | 70 | export {pages} 71 | -------------------------------------------------------------------------------- /frontend/src/pages/App.css: -------------------------------------------------------------------------------- 1 | #top-bar { 2 | position: sticky; 3 | z-index: 999; 4 | top: 0; 5 | width: 100%; 6 | } 7 | -------------------------------------------------------------------------------- /frontend/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 3 | // with the License. A copy of the License is located at 4 | // 5 | // http://aws.amazon.com/apache2.0/ 6 | // 7 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 8 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 9 | // limitations under the License. 10 | import {Html, Head, Main, NextScript} from 'next/document' 11 | 12 | export default function Document() { 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /frontend/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | const reportWebVitals = (onPerfEntry: any) => { 12 | if (onPerfEntry && onPerfEntry instanceof Function) { 13 | import('web-vitals').then(({getCLS, getFID, getFCP, getLCP, getTTFB}) => { 14 | getCLS(onPerfEntry) 15 | getFID(onPerfEntry) 16 | getFCP(onPerfEntry) 17 | getLCP(onPerfEntry) 18 | getTTFB(onPerfEntry) 19 | }) 20 | } 21 | } 22 | 23 | export default reportWebVitals 24 | -------------------------------------------------------------------------------- /frontend/src/types/instances.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | export enum InstanceState { 13 | Pending = 'pending', 14 | Running = 'running', 15 | ShuttingDown = 'shutting-down', 16 | Terminated = 'terminated', 17 | Stopping = 'stopping', 18 | Stopped = 'stopped', 19 | } 20 | 21 | export enum NodeType { 22 | HeadNode = 'HeadNode', 23 | ComputeNode = 'ComputeNode', 24 | } 25 | 26 | export type EC2Instance = { 27 | instanceId: string 28 | instanceType: string 29 | launchTime: string 30 | privateIpAddress: string // only primary? 31 | publicIpAddress: string 32 | state: InstanceState 33 | } 34 | 35 | export type Instance = { 36 | instanceId: string 37 | instanceType: string 38 | launchTime: string 39 | nodeType: NodeType 40 | privateIpAddress: string // only primary? 41 | publicIpAddress?: string 42 | queueName?: string 43 | state: InstanceState 44 | } 45 | -------------------------------------------------------------------------------- /frontend/src/types/logs.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | 12 | export type LogStreamName = string 13 | 14 | export type LogEvent = { 15 | message: string 16 | timestamp: string 17 | } 18 | 19 | export type LogEvents = LogEvent[] 20 | 21 | export type LogFilterList = LogFilterExpression[] 22 | 23 | export type LogFilterExpression = string 24 | 25 | export type LogStreams = LogStream[] 26 | 27 | export type LogStream = { 28 | // Name of the log stream. 29 | logStreamName: string 30 | // The creation time of the stream. 31 | creationTime: string 32 | // The time of the first event of the stream. 33 | firstEventTimestamp: string 34 | // The time of the last event of the stream. The lastEventTime value updates on an eventual consistency basis. It typically updates in less than an hour from ingestion, but in rare situations might take longer. 35 | lastEventTimestamp: string 36 | // The last ingestion time. 37 | lastIngestionTime: string 38 | // The sequence token. 39 | uploadSequenceToken: string 40 | // The Amazon Resource Name (ARN) of the log stream. 41 | logStreamArn: string 42 | } 43 | -------------------------------------------------------------------------------- /frontend/src/types/stackevents.tsx: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 4 | // with the License. A copy of the License is located at 5 | // 6 | // http://aws.amazon.com/apache2.0/ 7 | // 8 | // or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 9 | // OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 10 | // limitations under the License. 11 | import {CloudFormationResourceStatus} from './base' 12 | 13 | export type StackEvent = { 14 | // The unique ID name of the instance of the stack. 15 | stackId: string 16 | // The unique ID of this event. 17 | eventId: string 18 | // The name associated with a stack. 19 | stackName: string 20 | // The logical name of the resource specified in the template. 21 | logicalResourceId: string 22 | // The name or unique identifier associated with the physical instance of the resource. 23 | physicalResourceId: string 24 | // Type of resource. 25 | resourceType: string 26 | // Time the status was updated. 27 | timestamp: string 28 | // Current status of the resource. 29 | resourceStatus: CloudFormationResourceStatus 30 | // Success/failure message associated with the resource. 31 | resourceStatusReason?: string 32 | // BLOB of the properties used to create the resource. 33 | resourceProperties?: string 34 | // The token passed to the operation that generated this event. 35 | clientRequestToken?: string 36 | } 37 | 38 | export type StackEvents = StackEvent[] 39 | -------------------------------------------------------------------------------- /frontend/src/types/users.tsx: -------------------------------------------------------------------------------- 1 | export type UserAttributes = { 2 | email: string 3 | phone_number: string 4 | sub: string 5 | } 6 | 7 | export type UserGroup = { 8 | GroupName: string 9 | Description: string 10 | UserPoolId: string 11 | Precedence: number 12 | CreationDate: Date 13 | LastModifiedDate: Date 14 | } 15 | 16 | export enum UserStatus { 17 | Confirmed = 'CONFIRMED', 18 | Unconfirmed = 'UNCONFIRMED', 19 | ExternalProvider = 'EXTERNAL_PROVIDER', 20 | Archived = 'ARCHIVED', 21 | Unknown = 'UNKNOWN', 22 | ResetRequired = 'RESET_REQUIRED', 23 | ForceChangePassword = 'FORCE_CHANGE_PASSWORD', 24 | } 25 | 26 | export type User = { 27 | Attributes: UserAttributes 28 | Groups: UserGroup[] 29 | Username: string 30 | UserStatus: UserStatus 31 | Enabled: boolean 32 | UserCreateDate: Date 33 | UserLastModifiedDate: Date 34 | } 35 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "moduleResolution": "node", 17 | "resolveJsonModule": true, 18 | "isolatedModules": true, 19 | "jsx": "preserve", 20 | "sourceMap": true, 21 | "allowJs": true, 22 | "typeRoots": ["./node_modules/@types", "src/types"] 23 | }, 24 | "include": [ 25 | "next-env.d.ts", 26 | "**/*.ts", 27 | "**/*.tsx" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /infrastructure/bucket_configuration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 5 | # with the License. A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 10 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | REGULAR_BUCKET=parallelcluster-ui-release-artifacts-us-east-1 14 | ALTERNATIVE_BUCKET=parallelcluster-ui-release-artifacts-eu-west-1 15 | REGULAR_REGION_FOR_TEMPLATES="us-east-1" 16 | ALTERNATIVE_REGION_FOR_TEMPLATES="eu-west-1" 17 | 18 | BUCKETS=("$REGULAR_BUCKET" "$ALTERNATIVE_BUCKET") 19 | REGIONS=("$REGULAR_REGION_FOR_TEMPLATES" "$ALTERNATIVE_REGION_FOR_TEMPLATES") -------------------------------------------------------------------------------- /infrastructure/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 5 | # with the License. A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 10 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | error(){ 14 | echo "An error has occurred while releasing the infrastructure files." 15 | } 16 | 17 | -------------------------------------------------------------------------------- /infrastructure/environments/demo-cfn-update-args.yaml: -------------------------------------------------------------------------------- 1 | TemplateURL: BUCKET_URL_PLACEHOLDER/parallelcluster-ui.yaml 2 | Parameters: 3 | - ParameterKey: Version 4 | ParameterValue: 3.4.0 5 | - ParameterKey: InfrastructureBucket 6 | ParameterValue: BUCKET_URL_PLACEHOLDER 7 | - ParameterKey: UserPoolId 8 | UsePreviousValue: true 9 | - ParameterKey: UserPoolAuthDomain 10 | UsePreviousValue: true 11 | - ParameterKey: SNSRole 12 | UsePreviousValue: true 13 | - ParameterKey: PublicEcrImageUri 14 | ParameterValue: public.ecr.aws/pcm/pcluster-manager-awslambda:latest 15 | Capabilities: 16 | - CAPABILITY_AUTO_EXPAND 17 | - CAPABILITY_NAMED_IAM 18 | DisableRollback: false 19 | -------------------------------------------------------------------------------- /infrastructure/environments/demo-variables.sh: -------------------------------------------------------------------------------- 1 | REGION=eu-west-1 2 | STACK_NAME=parallelcluster-ui-demo 3 | INFRA_BUCKET_STACK_NAME=pcluster-manager-github -------------------------------------------------------------------------------- /infrastructure/release_infrastructure.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 6 | # with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 11 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 15 | 16 | source ${SCRIPT_DIR}/common.sh 17 | source ${SCRIPT_DIR}/bucket_configuration.sh 18 | trap 'error' ERR 19 | 20 | echo "Uploading the main templates" 21 | "${SCRIPT_DIR}"/upload.sh "$SCRIPT_DIR" 22 | echo "Uploading accounting template" 23 | "${SCRIPT_DIR}"/slurm-accounting/upload.sh "$SCRIPT_DIR" -------------------------------------------------------------------------------- /infrastructure/slurm-accounting/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 6 | # with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 11 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | source $1/common.sh 15 | source $1/bucket_configuration.sh 16 | trap 'error' ERR 17 | 18 | ACCOUNTING_SCRIPT_DIR="$1/slurm-accounting" 19 | 20 | if [ ! -d "$ACCOUNTING_SCRIPT_DIR" ] || [ ! -r "$ACCOUNTING_SCRIPT_DIR" ]; 21 | then 22 | echo "ACCOUNTING_SCRIPT_DIR=$ACCOUNTING_SCRIPT_DIR must be a readable directory" 23 | exit 1; 24 | fi 25 | 26 | FILES=(accounting-cluster-template.yaml) 27 | 28 | for INDEX in "${!BUCKETS[@]}" 29 | do 30 | echo Uploading to: "${BUCKETS[INDEX]}" 31 | for FILE in "${FILES[@]}" 32 | do 33 | aws s3 cp "${ACCOUNTING_SCRIPT_DIR}/${FILE}" "s3://${BUCKETS[INDEX]}/slurm-accounting/${FILE}" 34 | done 35 | done 36 | -------------------------------------------------------------------------------- /infrastructure/update_md_files_links.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 5 | # with the License. A copy of the License is located at 6 | # 7 | # http://aws.amazon.com/apache2.0/ 8 | # 9 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 10 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 11 | # limitations under the License. 12 | 13 | # This script is just an internal utility script to change all the 1-click links when the bucket changes 14 | 15 | source common.sh 16 | trap 'error' ERR 17 | 18 | REGIONS=( $(aws ec2 describe-regions --query "Regions[*].RegionName" --output text) ) 19 | SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" 20 | 21 | for REGION in "${REGIONS[@]}" 22 | do 23 | if [ "$REGION" != "ap-southeast-3" ] && [ "$REGION" != "me-central-1" ]; 24 | then 25 | #FIXME For other partitions we should also parametrize the partition in the URL 26 | while IFS= read -r -d '' file 27 | do 28 | if [[ "$file" != *"node_modules"* ]]; then 29 | # The commented one is for regions not yet on the new name (gov-cloud for example) 30 | #sed -i.bak -E "s/templateURL=.*${REGION}\.s3\.${REGION}\.amazonaws\.com\/(.*)yaml(.*)templateURL=.*${REGION}\.s3\.${REGION}\.amazonaws\.com\/(.*)yaml/templateURL=https:\/\/${REGULAR_BUCKET}\.s3\.${REGULAR_REGION_FOR_TEMPLATES}\.amazonaws\.com\/\1yaml\2templateURL=https:\/\/${ALTERNATIVE_BUCKET}\.s3\.${ALTERNATIVE_REGION_FOR_TEMPLATES}\.amazonaws\.com\/\3yaml/" "${file}" && rm "${file}.bak" 31 | sed -i.bak -E "s/templateURL=.*${REGULAR_BUCKET}\.s3\.${REGION}\.amazonaws\.com\/(.*)yaml(.*)templateURL=.*${REGULAR_BUCKET}\.s3\.${REGION}\.amazonaws\.com\/(.*)yaml/templateURL=https:\/\/${REGULAR_BUCKET}\.s3\.${REGULAR_REGION_FOR_TEMPLATES}\.amazonaws\.com\/\1yaml\2templateURL=https:\/\/${ALTERNATIVE_BUCKET}\.s3\.${ALTERNATIVE_REGION_FOR_TEMPLATES}\.amazonaws\.com\/\3yaml/" "${file}" && rm "${file}.bak" 32 | fi 33 | done < <(find "$SCRIPT_DIR/.." -name "*.md" -type f -print0) 34 | fi 35 | done 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /infrastructure/upload.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | # Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 4 | # 5 | # Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance 6 | # with the License. A copy of the License is located at 7 | # 8 | # http://aws.amazon.com/apache2.0/ 9 | # 10 | # or in the "LICENSE.txt" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES 11 | # OR CONDITIONS OF ANY KIND, express or implied. See the License for the specific language governing permissions and 12 | # limitations under the License. 13 | 14 | SCRIPT_DIR=$1 15 | 16 | source ${SCRIPT_DIR}/common.sh 17 | source ${SCRIPT_DIR}/bucket_configuration.sh 18 | trap 'error' ERR 19 | 20 | 21 | if [ -z "$SCRIPT_DIR" ]; 22 | then 23 | echo "SCRIPT_DIR=$SCRIPT_DIR must be initialized and not empty" 24 | exit 1; 25 | fi 26 | 27 | FILES=(SSMSessionProfile-cfn.yaml pcluster-manager-cognito.yaml pcluster-manager.yaml) 28 | 29 | for INDEX in "${!BUCKETS[@]}" 30 | do 31 | echo Uploading to: "${BUCKETS[INDEX]}" 32 | #FIXME For other partitions we should also parametrize the partition in the URL 33 | TEMPLATE_URL="https:\/\/${BUCKETS[INDEX]}\.s3\.${REGIONS[INDEX]}\.amazonaws\.com" 34 | sed -i "s/PLACEHOLDER/${TEMPLATE_URL}/g" "${SCRIPT_DIR}/pcluster-manager.yaml" 35 | for FILE in "${FILES[@]}" 36 | do 37 | aws s3 cp "${SCRIPT_DIR}/${FILE}" "s3://${BUCKETS[INDEX]}/${FILE}" 38 | done 39 | done 40 | -------------------------------------------------------------------------------- /pcluster_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/pcluster-manager/f6b5bdcfc49ad9664d9bcd4bd3952e55e7a62a86/pcluster_logo.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | Flask-Cors==3.0.10 2 | Flask==2.1.2 3 | boto3==1.24.30 4 | requests==2.28.1 5 | python-jose==3.3.0 6 | PyYAML==6.0 7 | pytest==7.1.2 8 | pytest-mock==3.8.2 9 | itsdangerous==2.1.2 10 | marshmallow==3.19.0 -------------------------------------------------------------------------------- /resources/files/sacct/slurm_sacct.conf.erb: -------------------------------------------------------------------------------- 1 | # ACCOUNTING 2 | JobAcctGatherType=jobacct_gather/linux 3 | JobAcctGatherFrequency=30 4 | # 5 | AccountingStorageType=accounting_storage/slurmdbd 6 | AccountingStorageHost=<%= @slurm_dbd_host %> 7 | AccountingStorageUser=<%= @slurm_db_user %> 8 | AccountingStoragePort=6819 9 | -------------------------------------------------------------------------------- /resources/files/sacct/slurmdbd.conf.erb: -------------------------------------------------------------------------------- 1 | ArchiveEvents=yes 2 | ArchiveJobs=yes 3 | ArchiveResvs=yes 4 | ArchiveSteps=no 5 | ArchiveSuspend=no 6 | ArchiveTXN=no 7 | ArchiveUsage=no 8 | AuthType=auth/munge 9 | DbdHost=<%= @slurm_dbd_host %> 10 | DbdPort=6819 11 | DebugLevel=info 12 | PurgeEventAfter=1month 13 | PurgeJobAfter=12month 14 | PurgeResvAfter=1month 15 | PurgeStepAfter=1month 16 | PurgeSuspendAfter=1month 17 | PurgeTXNAfter=12month 18 | PurgeUsageAfter=24month 19 | SlurmUser=slurm 20 | LogFile=/var/log/slurmdbd.log 21 | PidFile=/var/run/slurmdbd.pid 22 | StorageType=accounting_storage/mysql 23 | StorageUser=<%= @slurm_db_user %> 24 | StoragePass=<%= @slurm_db_password %> 25 | StorageHost=<%= node['slurm_accounting']['rds_endpoint'] %> 26 | StoragePort=<%= node['slurm_accounting']['rds_port'] %> 27 | -------------------------------------------------------------------------------- /resources/files/sacct/slurmdbd.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=SlurmDBD daemon 3 | After=munge.service network.target 4 | ConditionPathExists=/opt/slurm/etc/slurmdbd.conf 5 | StartLimitIntervalSec=0 6 | 7 | [Service] 8 | Type=simple 9 | Restart=always 10 | RestartSec=1 11 | User=root 12 | ExecStart=/opt/slurm/sbin/slurmdbd -D -s 13 | ExecReload=/bin/kill -HUP $MAINPID 14 | LimitNOFILE=65536 15 | 16 | [Install] 17 | WantedBy=multi-user.target 18 | -------------------------------------------------------------------------------- /scripts/README.md: -------------------------------------------------------------------------------- 1 | # How to use rollback/rollforward scripts 2 | 3 | The rollback and rollforward scripts in this folder should be used when there are problems on a published release. In particular in the frontend or backend code. 4 | 5 | To work properly, the scripts need Admin credentials of the account in which the public ECR is. 6 | 7 | As the code is shipped as a Docker-based Lambda in a public ECR, to rollback the code we need to remove the "latest" tag from the broken released image and assign it to the image that was pushed before (latest-1 release). 8 | So the customers won't pull the broken release again. 9 | 10 | This is done by simply launching 11 | 12 | ``` 13 | bash ./rollback_awslambda_image.sh 14 | ``` 15 | 16 | If the rollback script is launched by mistake, the rollforward script helps to restore the situation, putting the "latest" tag on the lastly pushed release, the script can be launched as 17 | 18 | ``` 19 | bash ./rollforward_awslambda_image.sh 20 | ``` 21 | 22 | **NOTE:** The rollforward script should be used only if the rollback script is launched by mistake, the normal procedure to publish a new release after a rollback is by launching the `build_and_release_image.sh` after fixing the code 23 | 24 | -------------------------------------------------------------------------------- /scripts/build_and_release_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | USAGE="$(basename "$0") [-h] [--tag YYYY.MM (defaults to current year and month)] [--revision REVISION]" 5 | 6 | get_default_pcui_version() { 7 | date +%Y.%m 8 | } 9 | 10 | print_usage() { 11 | echo "$USAGE" 1>&2 12 | } 13 | 14 | ECR_REPO=parallelcluster-ui 15 | 16 | 17 | while [[ $# -gt 0 ]] 18 | do 19 | key="$1" 20 | 21 | case $key in 22 | -h) 23 | print_usage 24 | exit 1 25 | ;; 26 | --tag) 27 | TAG=$2 28 | shift 29 | shift 30 | ;; 31 | --revision) 32 | REVISION=$2 33 | shift 34 | shift 35 | ;; 36 | *) 37 | print_usage 38 | exit 1 39 | ;; 40 | esac 41 | done 42 | 43 | ECR_ENDPOINT="public.ecr.aws/pcm" 44 | 45 | if [ -z $TAG ]; then 46 | TAG=`get_default_pcui_version` 47 | echo "Using default versioning strategy, tag: $TAG" 1>&2 48 | elif ! [[ $TAG =~ [0-9]{4}\.(0[1-9]|1[0-2]) ]]; then 49 | echo '`--tag` parameter must be a valid year and month combo of the following format `YYYY.MM`, exiting' 1>&2 50 | exit 1 51 | fi 52 | 53 | if ! [ -z $REVISION ]; then 54 | TAG="${TAG}.${REVISION}" 55 | echo "Using provided revision, tag: $TAG" 1>&2 56 | fi 57 | 58 | aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin "${ECR_ENDPOINT}" 59 | 60 | pushd frontend 61 | if [ ! -d node_modules ]; then 62 | npm install 63 | fi 64 | docker build --build-arg PUBLIC_URL=/ -t frontend-awslambda . 65 | popd 66 | docker build -f Dockerfile.awslambda -t ${ECR_REPO} . 67 | 68 | ECR_IMAGE_VERSION_TAGGED=${ECR_ENDPOINT}/${ECR_REPO}:${TAG} 69 | ECR_IMAGE_LATEST_TAGGED=${ECR_ENDPOINT}/${ECR_REPO}:latest 70 | 71 | docker tag ${ECR_REPO} ${ECR_IMAGE_VERSION_TAGGED} 72 | docker push ${ECR_IMAGE_VERSION_TAGGED} 73 | 74 | docker tag ${ECR_REPO} ${ECR_IMAGE_LATEST_TAGGED} 75 | docker push ${ECR_IMAGE_LATEST_TAGGED} 76 | 77 | echo "Uploaded: ${ECR_IMAGE_VERSION_TAGGED}, ${ECR_IMAGE_LATEST_TAGGED}" -------------------------------------------------------------------------------- /scripts/cognito-tools/.gitignore: -------------------------------------------------------------------------------- 1 | *.txt -------------------------------------------------------------------------------- /scripts/cognito-tools/README.md: -------------------------------------------------------------------------------- 1 | # Cognito Tools 2 | 3 | ## Features 4 | - user export with groups 5 | - user import with groups 6 | 7 | ## Requirements 8 | - AWS credentials in the form of ENV vars 9 | - AWS_ACCESS_KEY_ID 10 | - AWS_SECRET_ACCESS_KEY 11 | - AWS_SESSION_TOKEN 12 | Other form of AWS credentials could be used, the underline executable is the [AWSCli](https://docs.aws.amazon.com/cli/index.html) 13 | - AWS Cli installed 14 | 15 | # How to 16 | 17 | Get AWS credentials for the target account in which you want to operate. 18 | Paste those credentials in a terminal as usual. 19 | 20 | Then, to export users you need to get at least the region in which the Cognito user pool is. 21 | If you don't specify the user pool, the script will just grab the first user pool id available 22 | for the account in the specified region. 23 | 24 | ## Export users with groups 25 | To export users and groups you need 26 | - the region for the user pool 27 | - the user pool id (optional, if not specified the script will grab the first one returned by the API) 28 | 29 | So you can run either this 30 | ```bash 31 | ./export_cognito_users.sh --region eu-west-1 --pool-id eu-west-1_X0gPxTtR8 32 | ``` 33 | 34 | 35 | ## Import users with groups 36 | To import users with their respective groups, you need 37 | - the region for the user pool 38 | - the path to the export file 39 | - the user pool id (optional, if not specified the script will grab the first one returned by the API) 40 | - the temporary password to set for each user (optional, defaults to `P@ssw0rd`) 41 | - whether you want to send the email alerting users of the account creation or not 42 | 43 | Assuming you exported the users and groups to a file called `export.txt`, you can run 44 | ```bash 45 | ./import_cognito_users.sh --region eu-west-1 --users-export-file export.txt 46 | ``` -------------------------------------------------------------------------------- /scripts/cognito-tools/common.sh: -------------------------------------------------------------------------------- 1 | RED='\033[0;31m' 2 | NC='\033[0m' 3 | 4 | check_region() { 5 | if [ -z $REGION ]; then 6 | echo -e "${RED}Region is not set. Exiting${NC}" 1>&2 7 | exit 1 8 | fi 9 | } 10 | 11 | get_user_pool_id() { 12 | aws cognito-idp list-user-pools --region "$REGION" --max-results 1 --query 'UserPools[0].Id' --output text 13 | } 14 | 15 | check_user_pool_id() { 16 | if [ -z $USER_POOL_ID ]; then 17 | echo -e "${RED}Cognito user pool id is NOT set, using first user pool returned by the query${NC}" 1>&2 18 | USER_POOL_ID=`get_user_pool_id` 19 | echo -e "${RED}Chosen Cognito user pool id is ${USER_POOL_ID}${NC}" 1>&2 20 | fi 21 | } 22 | -------------------------------------------------------------------------------- /scripts/cognito-tools/export_cognito_users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | 4 | function print_help() { 5 | echo "Usage $0 --region REGION [--user-pool-id USER_POOL_ID]" 6 | } 7 | 8 | while [[ $# -gt 0 ]] 9 | do 10 | key="$1" 11 | 12 | case $key in 13 | -h) 14 | print_help >&2 15 | exit 1 16 | ;; 17 | 18 | --user-pool-id) 19 | USER_POOL_ID="$2" 20 | shift 21 | shift 22 | ;; 23 | 24 | --region) 25 | REGION=$2 26 | shift 27 | shift 28 | ;; 29 | 30 | *) # unknown option 31 | print_help >&2 32 | exit 1 33 | ;; 34 | esac 35 | done 36 | 37 | 38 | . common.sh 39 | 40 | check_region 41 | check_user_pool_id 42 | 43 | aws cognito-idp list-users --region "$REGION" --user-pool-id $USER_POOL_ID --query "Users[].{email: Attributes[?Name == 'email'].Value | [0]}" --output text | while read USERNAME 44 | do 45 | USER_GROUPS=$(aws cognito-idp admin-list-groups-for-user --username "$USERNAME" --user-pool-id "$USER_POOL_ID" --region "$REGION" --query 'Groups[*].GroupName' --output text | tr -s '\t' ',') 46 | echo "$USERNAME|$USER_GROUPS" 47 | done 48 | -------------------------------------------------------------------------------- /scripts/cognito-tools/import_cognito_users.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | function print_help() { 4 | echo "Usage $0 --region REGION --users-export-file PATH [--user-pool-id USER_POOL_ID --temp-pwd TEMP_PASSWORD --no-email]" 5 | } 6 | 7 | TEMP_PWD='P@ssw0rd' 8 | SEND_EMAIL=true 9 | 10 | while [[ $# -gt 0 ]] 11 | do 12 | key="$1" 13 | 14 | case $key in 15 | -h) 16 | print_help 1>&2 17 | exit 1 18 | ;; 19 | 20 | --user-pool-id) 21 | USER_POOL_ID="$2" 22 | shift 23 | shift 24 | ;; 25 | 26 | --region) 27 | REGION=$2 28 | shift 29 | shift 30 | ;; 31 | 32 | --temp-pwd) 33 | TEMP_PWD=$2 34 | shift 35 | shift 36 | ;; 37 | 38 | --no-email) 39 | SEND_EMAIL=false 40 | shift 41 | ;; 42 | 43 | --users-export-file) 44 | FILE=$2 45 | shift 46 | shift 47 | ;; 48 | 49 | *) # unknown option 50 | print_help >&2 51 | exit 1 52 | ;; 53 | esac 54 | done 55 | 56 | . common.sh 57 | 58 | check_region 59 | check_user_pool_id 60 | 61 | if [ -z "$FILE" ]; then 62 | echo "Users export file is required. Exiting" 1>&2 63 | exit 1 64 | fi 65 | 66 | cat "$FILE" | while read LINE; do 67 | EMAIL="$(echo "$LINE" | cut -d '|' -f 1)" 68 | USER_GROUPS="$(echo "$LINE" | cut -d '|' -f 2)" 69 | 70 | echo "Creating user $EMAIL with groups $USER_GROUPS" 71 | 72 | suppress_string='--message-action SUPPRESS' 73 | if [ $SEND_EMAIL = true ]; then 74 | suppress_string='' 75 | fi 76 | aws cognito-idp admin-create-user --region "$REGION" --user-pool-id "$USER_POOL_ID" --username "$EMAIL" --temporary-password "$TEMP_PWD" --user-attributes Name=email,Value="$EMAIL" Name=email_verified,Value=true $suppress_string > /dev/null 77 | echo $USER_GROUPS | tr ',' '\n' | while read USER_GROUP 78 | 79 | do 80 | aws cognito-idp admin-add-user-to-group --region "$REGION" --user-pool-id "$USER_POOL_ID" --username "$EMAIL" --group-name $USER_GROUP 81 | done 82 | done 83 | 84 | 85 | echo "All done" 86 | -------------------------------------------------------------------------------- /scripts/rollback_awslambda_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | REPO="parallelcluster-ui" 3 | DIGEST=$(aws ecr describe-images --repository-name "${REPO}" \ 4 | --query 'sort_by(imageDetails,& imagePushedAt)[-2].[imageDigest][0]') 5 | MANIFEST=$(aws ecr batch-get-image --repository-name "${REPO}" --image-ids imageDigest="${DIGEST}" | jq --raw-output --join-output '.images[0].imageManifest') 6 | aws ecr put-image --repository-name "${REPO}" --image-tag latest --image-manifest "${MANIFEST}" -------------------------------------------------------------------------------- /scripts/rollforward_awslambda_image.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | REPO="parallelcluster-ui" 3 | DIGEST=$(aws ecr describe-images --repository-name "${REPO}" \ 4 | --query 'sort_by(imageDetails,& imagePushedAt)[-1].[imageDigest][0]') 5 | MANIFEST=$(aws ecr batch-get-image --repository-name "${REPO}" --image-ids imageDigest="${DIGEST}" | jq --raw-output --join-output '.images[0].imageManifest') 6 | aws ecr put-image --repository-name "${REPO}" --image-tag latest --image-manifest "${MANIFEST}" -------------------------------------------------------------------------------- /scripts/run_flask.sh: -------------------------------------------------------------------------------- 1 | FLASK_RUN_PORT=5001 FLASK_APP=app:run flask run 2 | -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = app 3 | callable = app 4 | master = true 5 | --------------------------------------------------------------------------------