├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── pull_request_template.md ├── release.yml └── workflows │ ├── demo_deploy.yml │ ├── e2e_tests.yml │ ├── production_release.yml │ ├── publish-adrs.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 ├── costmonitoring │ ├── __init__.py │ ├── costexplorer_client.py │ └── costs.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 │ ├── costmonitoring │ │ └── test_costexplorer_client.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 │ ├── test_utils.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 ├── 20230222-make-revision-required.md ├── 20230310-scope-down-e2e-tests.md ├── 20230324-avoid-overriding-customer-ssm-sessionmanagerrunshell-removing-ssmsessionprofile-cfnyaml.md ├── 20230327-disable-fetch-on-focus.md ├── README.md ├── index.md └── template.md ├── e2e ├── .gitignore ├── configs │ ├── environment.ts │ └── login.ts ├── fixtures │ └── wizard.template.yaml ├── package-lock.json ├── package.json ├── playwright.config.ts ├── specs │ ├── images.spec.ts │ ├── logs.spec.ts │ ├── noMatch.spec.ts │ ├── users.spec.ts │ ├── wizard.fromcluster.spec.ts │ ├── wizard.spec.ts │ └── wizard.template.spec.ts └── test-utils │ ├── clusters.ts │ ├── login.ts │ ├── logs.ts │ ├── users.ts │ └── wizard.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 │ ├── img │ │ ├── 3P-Logos.NOTICE │ │ ├── ec2.svg │ │ ├── error_pages_illustration.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__ │ │ ├── ActivateCostMonitoring.test.ts │ │ ├── CreateCluster.test.ts │ │ ├── DescribeCluster.test.ts │ │ ├── GetCostMonitoringData.test.ts │ │ ├── GetCostMonitoringStatus.test.ts │ │ ├── GetVersion.test.ts │ │ ├── ListClusterLogEvents.test.ts │ │ ├── ListClusterLogStreams.test.ts │ │ ├── ListClusters.test.ts │ │ ├── console.test.tsx │ │ ├── storageCreationValidation.test.tsx │ │ └── util.test.tsx │ ├── app-config │ │ ├── __tests__ │ │ │ └── index.test.ts │ │ ├── index.ts │ │ └── types.ts │ ├── auth │ │ ├── __tests__ │ │ │ └── handleNotAuthorizedErrors.test.ts │ │ ├── constants.ts │ │ ├── handleNotAuthorizedErrors.ts │ │ └── types.ts │ ├── components │ │ ├── ConfigView.tsx │ │ ├── DeleteDialog.tsx │ │ ├── EmptyState.tsx │ │ ├── ErrorBoundary.tsx │ │ ├── FileChooser.tsx │ │ ├── HiddenFileUpload.tsx │ │ ├── InfoLink.tsx │ │ ├── InputErrors.tsx │ │ ├── Loading.tsx │ │ ├── NoMatch.tsx │ │ ├── SideBar.tsx │ │ ├── Status.tsx │ │ ├── TopBar.tsx │ │ ├── ValueWithLabel.tsx │ │ ├── __tests__ │ │ │ ├── ErrorBoundary.test.tsx │ │ │ ├── HiddenFileUpload.test.tsx │ │ │ ├── Status.test.tsx │ │ │ ├── TopBar.test.tsx │ │ │ ├── useClusterPoll.test.tsx │ │ │ └── useLoadingState.test.tsx │ │ ├── date │ │ │ ├── AbsoluteTimestamp.tsx │ │ │ ├── DateView.tsx │ │ │ └── __tests__ │ │ │ │ └── DateView.test.tsx │ │ ├── help-panel │ │ │ ├── DefaultHelpPanel.tsx │ │ │ ├── HelpPanel.tsx │ │ │ └── TitleDescriptionHelpPanel.tsx │ │ ├── useClusterPoll.tsx │ │ └── useLoadingState.tsx │ ├── css-properties.d.ts │ ├── 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 │ │ │ ├── Costs │ │ │ │ ├── CostData.tsx │ │ │ │ ├── EnableCostMonitoringButton.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── EnableCostMonitoringButton.test.tsx │ │ │ │ │ ├── composeTimeRange.test.ts │ │ │ │ │ ├── useActivateCostMonitoringMutation.test.tsx │ │ │ │ │ ├── useCostMonitoringStatus.test.tsx │ │ │ │ │ └── valueFormatter.test.ts │ │ │ │ ├── composeTimeRange.tsx │ │ │ │ ├── costs.queries.tsx │ │ │ │ ├── costs.types.ts │ │ │ │ ├── index.tsx │ │ │ │ └── valueFormatter.ts │ │ │ ├── CreateButtonDropdown │ │ │ │ ├── CreateButtonDropdown.tsx │ │ │ │ └── __tests__ │ │ │ │ │ └── CreateButtonDropdown.test.tsx │ │ │ ├── Details.tsx │ │ │ ├── Filesystems.tsx │ │ │ ├── FromClusterModal │ │ │ │ ├── FromClusterModal.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── FromClusterModal.test.tsx │ │ │ │ │ └── useClustersToCopyFrom.test.tsx │ │ │ │ └── useClustersToCopyFrom.ts │ │ │ ├── Instances.tsx │ │ │ ├── Properties.tsx │ │ │ ├── Scheduling.tsx │ │ │ ├── StackEvents.tsx │ │ │ ├── StopDialog.tsx │ │ │ ├── __tests__ │ │ │ │ ├── Clusters.test.tsx │ │ │ │ ├── Filesystems.test.ts │ │ │ │ ├── Tabs.test.tsx │ │ │ │ └── util.test.ts │ │ │ └── util.tsx │ │ ├── Configure │ │ │ ├── Cluster.tsx │ │ │ ├── Cluster │ │ │ │ ├── ClusterNameField.tsx │ │ │ │ ├── ImdsSupportFormField.tsx │ │ │ │ ├── OsFormField.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── ClusterNameField.test.tsx │ │ │ │ │ ├── ImdsSupportFormField.test.tsx │ │ │ │ │ ├── OsFormField.test.tsx │ │ │ │ │ └── clusterName.validators.test.ts │ │ │ │ └── clusterName.validators.ts │ │ │ ├── Components.module.css │ │ │ ├── Components.tsx │ │ │ ├── Components.types.ts │ │ │ ├── Configure.tsx │ │ │ ├── Create.tsx │ │ │ ├── Create.types.ts │ │ │ ├── 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 │ │ │ │ │ └── validateQueueName.test.ts │ │ │ │ ├── queues.mapper.ts │ │ │ │ ├── queues.types.ts │ │ │ │ └── queues.validators.tsx │ │ │ ├── SlurmSettings │ │ │ │ ├── QueueUpdateStrategyForm.tsx │ │ │ │ ├── ScaledownIdleTimeForm.tsx │ │ │ │ ├── SlurmAccountingForm.tsx │ │ │ │ ├── SlurmSettings.tsx │ │ │ │ └── __tests__ │ │ │ │ │ ├── QueueUpdateStrategyForm.test.tsx │ │ │ │ │ ├── ScaledownIdleTimeForm.test.tsx │ │ │ │ │ ├── SlurmSettings.test.tsx │ │ │ │ │ └── slurmAccountingValidation.test.ts │ │ │ ├── Storage.tsx │ │ │ ├── Storage.types.ts │ │ │ ├── Storage │ │ │ │ ├── AddStorageForm.tsx │ │ │ │ ├── DeletionPolicyFormField.tsx │ │ │ │ ├── __tests__ │ │ │ │ │ ├── AddStorageForm.test.tsx │ │ │ │ │ ├── DeletionPolicyFormField.test.tsx │ │ │ │ │ ├── buildStorageEntries.test.ts │ │ │ │ │ ├── mapStorageToUiSettings.test.ts │ │ │ │ │ ├── validateEbs.test.ts │ │ │ │ │ ├── validateEfs.test.ts │ │ │ │ │ ├── validateExternalFileSystem.test.ts │ │ │ │ │ └── validateStorageName.test.ts │ │ │ │ ├── buildStorageEntries.ts │ │ │ │ ├── storage.mapper.ts │ │ │ │ └── storage.validators.ts │ │ │ ├── __tests__ │ │ │ │ ├── Components.test.tsx │ │ │ │ ├── Configure.test.tsx │ │ │ │ ├── IMDSSecuredSettings.test.tsx │ │ │ │ ├── MultiUser.test.tsx │ │ │ │ ├── dynamicFS.test.tsx │ │ │ │ ├── errorsToFlashbarItems.test.ts │ │ │ │ ├── itemToOption.test.ts │ │ │ │ ├── storage.test.tsx │ │ │ │ └── useWizardNavigation.test.tsx │ │ │ ├── errorsToFlashbarItems.ts │ │ │ ├── useWizardNavigation.ts │ │ │ └── util.tsx │ │ ├── Images │ │ │ ├── CustomImages │ │ │ │ ├── CustomImageDetails.tsx │ │ │ │ ├── CustomImageStackEvents.tsx │ │ │ │ ├── CustomImages.tsx │ │ │ │ └── ImageBuildDialog.tsx │ │ │ ├── ImagesSplitPanel.tsx │ │ │ ├── OfficialImages │ │ │ │ └── OfficialImages.tsx │ │ │ └── index.tsx │ │ ├── Layout.tsx │ │ ├── Logs │ │ │ ├── LogMessagesTable.tsx │ │ │ ├── LogStreamsTable.tsx │ │ │ ├── __tests__ │ │ │ │ ├── LogMessagesTable.test.tsx │ │ │ │ ├── LogStreamsTable.test.tsx │ │ │ │ └── withNodeType.test.ts │ │ │ ├── index.tsx │ │ │ └── withNodeType.ts │ │ └── Users │ │ │ ├── AddUserModal.tsx │ │ │ ├── Users.tsx │ │ │ └── __tests__ │ │ │ ├── AddUserModal.test.tsx │ │ │ └── userValidate.test.ts │ ├── pages │ │ ├── App.css │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ └── index.tsx │ ├── reportWebVitals.ts │ ├── shared │ │ ├── __tests__ │ │ │ └── extendCollectionsOptions.test.ts │ │ ├── extendCollectionsOptions.ts │ │ └── propertyFilterI18nStrings.ts │ ├── store.ts │ ├── types │ │ ├── base.tsx │ │ ├── clusters.tsx │ │ ├── images.tsx │ │ ├── instances.tsx │ │ ├── jobs.tsx │ │ ├── logs.tsx │ │ ├── stackevents.tsx │ │ └── users.tsx │ └── util.ts ├── styled-jsx.d.ts └── tsconfig.json ├── infrastructure ├── README.md ├── bucket_configuration.sh ├── common.sh ├── custom-domain │ └── custom-domain.yaml ├── environments │ ├── demo-cfn-create-args.yaml │ ├── demo-cfn-update-args.yaml │ └── demo-variables.sh ├── github-env-setup-prod.yml ├── github-env-setup.yml ├── parallelcluster-ui-cognito.yaml ├── parallelcluster-ui.yaml ├── private-deployment │ └── private-deployment.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-dev.txt ├── requirements.in ├── 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 ├── common.sh ├── deploy.sh ├── rollback_awslambda_image.sh ├── rollforward_awslambda_image.sh ├── run_flask.sh ├── setup-env.sh └── tail-logs.sh └── uwsgi.ini /.dockerignore: -------------------------------------------------------------------------------- 1 | frontend 2 | frontend/* 3 | ./frontend/* 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: PCUI is malfunctioning 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Before submitting a ticket, please search through the following resources:** 11 | - [Official documentation](https://docs.aws.amazon.com/parallelcluster/latest/ug/pcui-using-v3.html) 12 | - [Issues](https://github.com/aws/aws-parallelcluster-ui/issues) 13 | 14 | ## Description 15 | Briefly describe the issue 16 | 17 | ## Steps to reproduce the issue 18 | List the steps you took to reproduce the issue 19 | 20 | ## Expected behaviour 21 | Describe what you expected to happen 22 | 23 | ## Actual behaviour 24 | Describe what happened instead. Please include, if relevant, any of the following: 25 | - error messages you see 26 | - screenshots 27 | 28 | ## Required info 29 | In order to help us determine the root cause of the issue, please provide the following information: 30 | - Region where ParallelCluster UI is installed 31 | - Version of ParallelCluster UI and ParallelCluster (follow [this guide](https://docs.aws.amazon.com/parallelcluster/parallelcluster/latest/ug/install-pcui-v3.html) to see what's installed) 32 | - Logs 33 | 34 | ## Additional info 35 | The following information is not required but helpful: 36 | - OS: [e.g. MacOS] 37 | - Browser [e.g. chrome, safari] 38 | 39 | ## If having problems with cluster creation or update 40 | YAML file generated by the ParallelCluster UI 41 | 42 | ## If having problems with custom image creation 43 | YAML file of the custom image 44 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | ## Description 3 | 4 | 5 | 6 | ## Changes 7 | 8 | 9 | 10 | ## How Has This Been Tested? 11 | 12 | 13 | 14 | ## References 15 | 16 | 17 | 18 | ## PR Quality Checklist 19 | 20 | - [ ] I added tests to new or existing code 21 | - [ ] I removed hardcoded strings and used [`react-i18next`](https://react.i18next.com/) library ([useTranslation hook](https://react.i18next.com/latest/usetranslation-hook) and/or [Trans component](https://react.i18next.com/latest/trans-component)), see an example [here](https://github.com/aws/aws-parallelcluster-ui/commit/a6f1e2aa46b245b5bf7500a04b83195477a5cfa5) 22 | - [ ] 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.) 23 | - [ ] I made sure that any [GitHub issue](https://github.com/aws/aws-parallelcluster-ui/issues) solved by this PR is correctly linked 24 | - [ ] I checked that infrastructure/update_infrastructure.sh runs without any error 25 | - [ ] I checked that `npm run build` builds without any error 26 | - [ ] I checked that clusters are listed correctly 27 | - [ ] I checked that a new cluster can be created (config is produced and dry run passes) 28 | - [ ] I checked that login and logout work as expected 29 | 30 | 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) 31 | 32 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 33 | -------------------------------------------------------------------------------- /.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 | 32 | - title: Known issues 33 | labels: 34 | - bug -------------------------------------------------------------------------------- /.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/publish-adrs.yml: -------------------------------------------------------------------------------- 1 | name: Publish ADRs 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | build-and-publish: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v2.3.4 12 | with: 13 | persist-credentials: false # required by JamesIves/github-pages-deploy-action 14 | fetch-depth: 0 # required by Log4brains to work correctly (needs the whole Git history) 15 | - name: Install Node 16 | uses: actions/setup-node@v1 17 | with: 18 | node-version: "14" 19 | - name: Install and Build Log4brains 20 | run: | 21 | npm install -g log4brains 22 | log4brains build --basePath /${GITHUB_REPOSITORY#*/}/log4brains 23 | - name: Deploy 24 | uses: JamesIves/github-pages-deploy-action@3.7.1 25 | with: 26 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 27 | BRANCH: gh-pages 28 | FOLDER: .log4brains/out 29 | TARGET_FOLDER: log4brains 30 | -------------------------------------------------------------------------------- /.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-latest 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-latest 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 | .ignore 13 | infrastructure/environments/* 14 | !infrastructure/environments/demo-* 15 | -------------------------------------------------------------------------------- /.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 UI (PCUI) 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.12-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.12-x86_64 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 internationalized any user-facing text or copy, by using [`react-i18next`](https://react.i18next.com/) library ([useTranslation hook](https://react.i18next.com/latest/usetranslation-hook) and/or [Trans component](https://react.i18next.com/latest/trans-component)) so that we can translate to other languages in the future, see an example [here](https://github.com/aws/aws-parallelcluster-ui/commit/a6f1e2aa46b245b5bf7500a04b83195477a5cfa5) 8 | - You have followed the PR Quality Checklist available in our [Pull Request Template](.github/pull_request_template.md) 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AWS ParallelCluster UI 2 | ================================ 3 | This project is a front-end for [AWS ParallelCluster](https://github.com/aws/aws-parallelcluster) 4 | 5 | Quickly and easily create HPC cluster in AWS using AWS ParallelCluster UI. This UI uses the AWS ParallelCluster 3.x API to Create, Update and Delete Clusters as well as access, view logs, and build Amazon Machine Images (AMI's). 6 | 7 | ## Install 8 | See [Official documentation](https://docs.aws.amazon.com/parallelcluster/latest/ug/install-pcui-v3.html) to install ParallelCluster UI. 9 | ## Development 10 | 11 | See [Development guide](DEVELOPMENT.md) to setup a local environment. 12 | 13 | ## Security 14 | 15 | See [Security Issue Notifications](CONTRIBUTING.md#security-issue-notifications) for more information. 16 | 17 | ## Contributing 18 | 19 | Please refer to our [Contributing Guidelines](CONTRIBUTING.md) before reporting bugs or feature requests. 20 | 21 | Please refer to our [Project Guidelines](PROJECT_GUIDELINES.md) before diving into the code. 22 | 23 | ## License 24 | 25 | This project is licensed under the Apache-2.0 License. 26 | 27 | [![PCUI ADRs](https://aws.github.io/aws-parallelcluster-ui/log4brains/badge.svg)](https://aws.github.io/aws-parallelcluster-ui/log4brains/) 28 | -------------------------------------------------------------------------------- /api/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws/aws-parallelcluster-ui/5703ff906f67a7ddf6c30e74a7b52f41091ef98c/api/__init__.py -------------------------------------------------------------------------------- /api/costmonitoring/__init__.py: -------------------------------------------------------------------------------- 1 | from .costs import costs -------------------------------------------------------------------------------- /api/costmonitoring/costs.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | import boto3 4 | from flask import Blueprint, request, jsonify 5 | 6 | from .costexplorer_client import CostExplorerClient, CostExplorerNotActiveException 7 | from ..PclusterApiHandler import authenticated 8 | from ..pcm_globals import logger 9 | from ..security.csrf.csrf import csrf_needed 10 | from ..utils import to_utc_datetime 11 | from ..validation import validated 12 | from ..validation.schemas import GetCostData 13 | 14 | CACHED_RESPONSE_MAX_AGE = 60 * 60 * 12 15 | 16 | COST_ALLOCATION_TAGS = ['parallelcluster:cluster-name'] 17 | 18 | costs = Blueprint('costs', __name__) 19 | 20 | costexplorer = boto3.client('ce') 21 | client = CostExplorerClient(costexplorer, cost_allocation_tags=COST_ALLOCATION_TAGS) 22 | 23 | 24 | @costs.get('') 25 | @authenticated({'admin'}) 26 | def cost_monitoring_status(): 27 | active = client.is_active() 28 | return {'active': active}, 200 29 | 30 | 31 | @costs.put('') 32 | @authenticated({'admin'}) 33 | @csrf_needed 34 | def activate_cost_monitoring(): 35 | client.activate() 36 | return {}, 204 37 | 38 | 39 | @costs.get('/clusters/') 40 | @authenticated({'admin'}) 41 | @validated(params=GetCostData) 42 | def get_cost_data_for(cluster_name): 43 | start = to_utc_datetime(request.args.get('start')).date().isoformat() 44 | end = to_utc_datetime(request.args.get('end', default=datetime.today().isoformat())).date().isoformat() 45 | 46 | cost_amounts = client.get_cost_data(cluster_name=cluster_name, start=start, end=end) 47 | 48 | return jsonify({'costs': cost_amounts}) 49 | 50 | 51 | @costs.errorhandler(CostExplorerNotActiveException) 52 | def handle_costexplorer_not_init_error(err): 53 | code, description = 405, str(err) 54 | logger.error(description, extra=dict(status=code, exception=type(err))) 55 | return {'code': code, 'message': str(err)}, code 56 | -------------------------------------------------------------------------------- /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 | def is_alphanumeric_with_hyphen(arg: str): 20 | pattern = re.compile(r"^[a-zA-Z][a-zA-Z0-9-]+$") 21 | return bool(re.fullmatch(pattern, arg)) 22 | 23 | 24 | aws_region_validator = validate.OneOf(choices=PC_REGIONS) 25 | 26 | 27 | def valid_api_log_levels_predicate(loglevel): 28 | return loglevel.lower() in VALID_LOG_LEVELS 29 | 30 | def size_not_exceeding(data, size): 31 | bytes_ = bytes(json.dumps(data), 'utf-8') 32 | byte_size = len(bytes_) 33 | if byte_size > size: 34 | raise ValidationError(f'Request body exceeded max size of {size} bytes') -------------------------------------------------------------------------------- /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: superseded by [20230222-make-revision-required](20230222-make-revision-required.md) 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 | ## Decision 13 | Using the format YYYY.MM[.REVISION] for PC UI versions, where 14 | 15 | - YYYY is the full year in which PC UI gets released 16 | - MM is the two digit number for the month in which PC UI gets released 17 | - REVISION is an optional value to allow separation of patches between the same major version 18 | 19 | -------------------------------------------------------------------------------- /decisions/20230222-make-revision-required.md: -------------------------------------------------------------------------------- 1 | # Make revision required 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe Team 5 | - Tags: versioning, pcui 6 | 7 | ## Context 8 | Currently, the versioning schema adopted takes inspiration from [CalVer](https://calver.org/) and it is composed of Year, Month and Revision number. 9 | 10 | As of today, Revision number is optional, and it is not automatically added by the release workflow. 11 | 12 | ## Decision 13 | We are making the Revision required, thus the new schema is now YYYY.MM.REVISION 14 | 15 | ## Consequences 16 | It is now possible to automatically release patches, without manual interventions. Revision number is taken from the GitHub ref name, as implemented with [#55](https://github.com/aws/aws-parallelcluster-ui/pull/55) 17 | 18 | ## Links 19 | - [PR](https://github.com/aws/aws-parallelcluster-ui/pull/55) 20 | - Supersedes [20230125-pcui-versioning-strategy](20230125-pcui-versioning-strategy.md) 21 | -------------------------------------------------------------------------------- /decisions/20230310-scope-down-e2e-tests.md: -------------------------------------------------------------------------------- 1 | # Scope down e2e tests 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe team 5 | - Date: 2023-03-10 6 | - Tags: e2e, tests 7 | 8 | ## Context 9 | Typically, e2e tests consist in simulating user interactions and making sure all the components of the system under test behave as expected. This involves reducing the number of mocks to the minimum (e.g. - only expensive third party APIs). 10 | 11 | In the case of PCUI, this would involve a lot of infrastructure being moved (e.g. creating a cluster) and it would add many dependencies to every test (ParallelCluster, the underlying AWS infrastructure, CloudFormation and so on). It would make the tests very costly, with little to no actual improvement in the confidence that PCUI is working as expected. 12 | 13 | Last but not least, PC provides a powerful dry-run feature that allows PCUI to quickly verify most of its features. 14 | 15 | ## Decision 16 | In PCUI, e2e tests will avoid involving costly tests such as an actual cluster creation 17 | 18 | ## Consequences 19 | - Cheaper, much quicker tests 20 | - More or less the same level of confidence in the stability of PCUI -------------------------------------------------------------------------------- /decisions/20230324-avoid-overriding-customer-ssm-sessionmanagerrunshell-removing-ssmsessionprofile-cfnyaml.md: -------------------------------------------------------------------------------- 1 | # Avoid overriding customer SSM-SessionManagerRunShell removing SSMSessionProfile-cfn.yaml 2 | 3 | - Status: accepted 4 | - Deciders: Nuraghe team 5 | - Date: 2023-03-24 6 | - Tags: SessionManager, SSM, Shell 7 | 8 | ## Context 9 | Customer SSM-SessionManagerRunShell gets overridden by PCUI Cloudformation template, possibly causing loss of 10 | customer's SSM shell preferences for the region in cui PCUI is deployed. 11 | 12 | ## Decision 13 | Remove the cloudformation substack SSMSessionProfile-cfn.yaml. 14 | With this change in place, when customers use the Shell button to connect to an SSM enabled cluster headnode, 15 | they will end up being logged in as the default SSM user (which is `ssm-user`) instead of the "default" ParallelCluster user. 16 | Nothing else will change for them. 17 | 18 | ## Links 19 | - [SSMSessionProfile-cfn.yaml](https://github.com/aws/aws-parallelcluster-ui/blob/1a6260ad60cd6bf5160e9b607e2dea85af9428e7/infrastructure/SSMSessionProfile-cfn.yaml) 20 | -------------------------------------------------------------------------------- /decisions/20230327-disable-fetch-on-focus.md: -------------------------------------------------------------------------------- 1 | # Disable fetch on focus 2 | 3 | - Status: accepted 4 | - Tags: frontend, data, react-query 5 | 6 | ## Context 7 | [`react-query`](https://github.com/tanstack/query) by default re-fetches active queries on window focus. This causes the UI to activate its loading animations, if there's any. This is a good behaviour for a PWA or other specific type of data visualizion, but it's less relevant for PCUI. 8 | 9 | ## Decision 10 | We [disabled](https://github.com/aws/aws-parallelcluster-ui/pull/126/commits/1e60ffaf2532b87353e283139b5d790622b1fd5d) this default behaviour, and will activate on a per-query basis in case of need. 11 | 12 | ## Consequences 13 | Data needs to intentionally refreshed either by developers or by users via an appropriate Refresh button. -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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://pcui-demo.nuraghe.team' 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/images.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 | 14 | test.describe('Given an endpoint where AWS ParallelCluster UI is deployed', () => { 15 | test.describe('when the user navigates to the Images page', () => { 16 | test('the user can switch between Official and Custom images sections', async ({ page }) => { 17 | await visitAndLogin(page) 18 | 19 | await page.getByRole('link', { name: 'Images' }).click(); 20 | 21 | await page.getByRole('tab', { name: /Official/i }).click(); 22 | await expect(page.getByRole('heading', { name: /Official images/ })).toBeVisible() 23 | 24 | await page.getByRole('tab', { name: /Custom/i }).click(); 25 | await expect(page.getByRole('heading', { name: /Custom images/ })).toBeVisible() 26 | }); 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /e2e/specs/logs.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 { test } from '@playwright/test'; 12 | import { ClusterAction, selectCluster, selectClusterAction } from '../test-utils/clusters'; 13 | import { visitAndLogin } from '../test-utils/login'; 14 | import { visitClusterLogsPage } from '../test-utils/logs'; 15 | 16 | test.describe('environment: @demo', () => { 17 | test.describe('Given an endpoint where AWS ParallelCluster UI is deployed', () => { 18 | test.describe('when the user selects a cluster', () => { 19 | test('the user can navigate to the cluster logs page', async ({ page }) => { 20 | await visitAndLogin(page) 21 | 22 | await selectCluster(page) 23 | 24 | await selectClusterAction(page, ClusterAction.VIEW_LOGS) 25 | 26 | await visitClusterLogsPage(page) 27 | }); 28 | }) 29 | }) 30 | }) 31 | -------------------------------------------------------------------------------- /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/specs/wizard.fromcluster.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 { test } from '@playwright/test'; 12 | import { visitAndLogin } from '../test-utils/login'; 13 | import { fillWizard } from '../test-utils/wizard'; 14 | 15 | const CLUSTER_TO_COPY_FROM = 'DO-NOT-DELETE' 16 | 17 | test.describe('environment: @demo', () => { 18 | test.describe('given an already existing cluster', () => { 19 | test.describe('when the cluster is picked as source to start the creation wizard', () => { 20 | test('user can perform a dry-run successfully', async ({ page }) => { 21 | await visitAndLogin(page) 22 | 23 | await page.getByRole('button', { name: 'Create cluster' }).first().click(); 24 | await page.getByRole('menuitem', { name: 'With an existing cluster' }).click(); 25 | 26 | await page.getByRole('button', { name: 'Select a cluster' }).click(); 27 | await page.getByRole('option', { name: CLUSTER_TO_COPY_FROM }).click(); 28 | await page.getByRole('dialog').getByRole('button', { name: 'Create' }).click(); 29 | 30 | await fillWizard(page) 31 | }); 32 | }) 33 | }) 34 | }) -------------------------------------------------------------------------------- /e2e/specs/wizard.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 { test } from '@playwright/test'; 12 | import { visitAndLogin } from '../test-utils/login'; 13 | import { fillWizard } from '../test-utils/wizard'; 14 | 15 | test.describe('Given an endpoint where AWS ParallelCluster UI is deployed', () => { 16 | test('a user should be able to login, navigate till the end of the cluster creation wizard, and perform a dry-run successfully', async ({ page }) => { 17 | await visitAndLogin(page) 18 | 19 | await page.getByRole('button', { name: 'Create cluster' }).first().click(); 20 | await page.getByRole('menuitem', { name: 'Step by step' }).click(); 21 | 22 | await fillWizard(page, {vpc: /vpc-.*/, region: 'eu-west-1'}) 23 | }); 24 | }) 25 | -------------------------------------------------------------------------------- /e2e/specs/wizard.template.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 { FileChooser, test } from '@playwright/test'; 12 | import { visitAndLogin } from '../test-utils/login'; 13 | import { fillWizard } from '../test-utils/wizard'; 14 | 15 | const TEMPLATE_PATH = './fixtures/wizard.template.yaml' 16 | 17 | test.describe('environment: @demo', () => { 18 | test.describe('given a cluster configuration template created with single instance type', () => { 19 | test.describe('when the file is imported as a template', () => { 20 | test('user can perform a dry-run successfully', async ({ page }) => { 21 | await visitAndLogin(page) 22 | 23 | await page.getByRole('button', { name: 'Create cluster' }).first().click(); 24 | 25 | page.on("filechooser", (fileChooser: FileChooser) => { 26 | fileChooser.setFiles([TEMPLATE_PATH]); 27 | }) 28 | await page.getByRole('menuitem', { name: 'With a template' }).click(); 29 | 30 | await fillWizard(page) 31 | }); 32 | }); 33 | }); 34 | }); -------------------------------------------------------------------------------- /e2e/test-utils/clusters.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 | 12 | import { Page } from "@playwright/test"; 13 | 14 | const DEFAULT_CLUSTER_TO_SELECT = 'DO-NOT-DELETE' 15 | 16 | export async function selectCluster(page: Page, clusterName: string = DEFAULT_CLUSTER_TO_SELECT) { 17 | await page.getByRole('row', { name: clusterName }) 18 | .getByRole('radio') 19 | .click(); 20 | } 21 | 22 | export enum ClusterAction { 23 | VIEW_LOGS = 'View logs' 24 | } 25 | 26 | export async function selectClusterAction(page: Page, action: ClusterAction) { 27 | await page.getByRole('button', { name: 'Actions', exact: true }).click(); 28 | await page.getByRole('menuitem', { name: action, exact: true }).click(); 29 | } -------------------------------------------------------------------------------- /e2e/test-utils/login.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 | 12 | import { Page } from "@playwright/test"; 13 | import { ENVIRONMENT_CONFIG } from "../configs/environment"; 14 | import { LOGIN_CONFIG } from "../configs/login"; 15 | 16 | export async function visitAndLogin(page: Page) { 17 | await page.goto(ENVIRONMENT_CONFIG.URL); 18 | await page.getByRole('textbox', { name: 'name@host.com' }).fill(LOGIN_CONFIG.username); 19 | await page.getByRole('textbox', { name: 'Password' }).fill(LOGIN_CONFIG.password); 20 | await page.getByRole('button', { name: 'submit' }).click(); 21 | } -------------------------------------------------------------------------------- /e2e/test-utils/logs.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 | 12 | import { expect, Page } from "@playwright/test"; 13 | 14 | const NON_ZERO_MESSAGES_COUNT = /Messages.*\([1-9][0-9]*\+\)/ 15 | 16 | export async function visitClusterLogsPage(page: Page) { 17 | await expect(page.getByRole('heading', { name: 'Cluster DO-NOT-DELETE logs', exact: true })).toBeVisible() 18 | await expect(page.getByRole('heading', { name: 'Log streams' })).toBeVisible() 19 | await expect(page.getByRole('heading', { name: 'Messages' })).toBeVisible() 20 | 21 | const logStreamsFilter = page.getByPlaceholder('Filter log streams') 22 | await logStreamsFilter.click(); 23 | await logStreamsFilter.fill('clusterstatusmgtd'); 24 | await logStreamsFilter.press('Enter'); 25 | 26 | await page.getByRole('row', { name: 'clusterstatusmgtd' }) 27 | .getByRole('radio') 28 | .click(); 29 | 30 | await expect(page.getByRole('heading', { name: NON_ZERO_MESSAGES_COUNT })).toBeVisible() 31 | } -------------------------------------------------------------------------------- /e2e/test-utils/users.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 | 12 | import { Page } from "@playwright/test"; 13 | 14 | export function generateUserEmail() { 15 | return `user${Math.random().toString(20).substring(2, 8)}@${Math.random().toString(20).substring(2, 6)}.com` 16 | } 17 | 18 | export async function addUser(page: Page, email: string) { 19 | await page.getByRole('button', { name: 'Add user' }).first().click(); 20 | await page.getByPlaceholder('email@domain.com').fill(email); 21 | await page.getByRole('button', { name: 'Add user' }).nth(1).click() 22 | } 23 | 24 | export async function deleteUser(page: Page, email: string) { 25 | selectUser(page, email) 26 | await page.getByRole('button', { name: 'Remove' }).first().click(); 27 | await page.getByRole('button', { name: 'Delete' }).first().click(); 28 | } 29 | 30 | export async function selectUser(page: Page, email: string) { 31 | await page.getByRole('row', { name: email }).getByRole('radio').click(); 32 | } 33 | 34 | export async function findUser(page: Page, email: string) { 35 | await page.getByPlaceholder('Find users').click(); 36 | await page.getByPlaceholder('Find users').fill(email); 37 | } 38 | -------------------------------------------------------------------------------- /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 | "ignorePatterns": [ 7 | "**/jest.config.js" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /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 | 18 -------------------------------------------------------------------------------- /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:22.14-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 | // necessary to circumvent Jest exception handlers to test exception throwing 27 | // this is due to the impossibility to run code before Jest starts, since Jest performs its 28 | // override/patching of the `process` variable 29 | // see https://johann.pardanaud.com/blog/how-to-assert-unhandled-rejection-and-uncaught-exception-with-jest/ 30 | // this is not the right place for setup code or custom configuration code 31 | // do NOT add code here 32 | process._original = (function (_original) { 33 | return function () { 34 | return _original 35 | } 36 | })(process) 37 | 38 | module.exports = mergePolarisPreset 39 | -------------------------------------------------------------------------------- /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 | output: 'export', 14 | distDir: 'build', 15 | reactStrictMode: true, 16 | basePath: '/pcui', 17 | images: { 18 | unoptimized: true, 19 | }, 20 | async rewrites() { 21 | return [ 22 | /** 23 | * Rewrite everything to `pages/index` 24 | * 25 | * This is only here because as of yet we are not 26 | * relying on NextJS routing and react-router-dom 27 | * does not play well with SSR. 28 | * 29 | * While doing the transition to NextJS routing, 30 | * we need a way to support both ways of functioning. 31 | * 32 | * Please note, this is only useful in the context of 33 | * local development (`npm run dev`), as this app is 34 | * currently being built as a static export 35 | * and no rewrite is going to be actually run in production 36 | */ 37 | { 38 | source: "/:any*", 39 | destination: "/", 40 | }, 41 | ]; 42 | }, 43 | } 44 | 45 | const withTM = require("next-transpile-modules")([ 46 | "@cloudscape-design/components", 47 | "@cloudscape-design/component-toolkit", 48 | "@cloudscape-design/design-tokens" 49 | ]); 50 | 51 | module.exports = withTM(nextConfig); 52 | -------------------------------------------------------------------------------- /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/ec2.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/public/img/error_pages_illustration.svg: -------------------------------------------------------------------------------- 1 | 26 | -------------------------------------------------------------------------------- /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/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/auth/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 UserRole = 'admin' 12 | 13 | export interface UserIdentity { 14 | attributes: {email: string} 15 | user_roles: UserRole[] 16 | username: string 17 | } 18 | -------------------------------------------------------------------------------- /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 | interface Props { 16 | title: string 17 | subtitle: string 18 | action?: React.ReactElement 19 | } 20 | 21 | export default function EmptyState({title, subtitle, action}: Props) { 22 | return ( 23 | 24 | 25 | {title} 26 | 27 | 28 | {subtitle} 29 | 30 | {action} 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/FileChooser.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 {Button} from '@cloudscape-design/components' 15 | 16 | // State 17 | import {useTranslation} from 'react-i18next' 18 | 19 | function FileUploadButton(props: any) { 20 | const {t} = useTranslation() 21 | const hiddenFileInput = React.useRef(null) 22 | const handleClick = (event: any) => { 23 | // @ts-expect-error TS(2531) FIXME: Object is possibly 'null'. 24 | hiddenFileInput.current.click() 25 | } 26 | const handleChange = (event: any) => { 27 | var file = event.target.files[0] 28 | var reader = new FileReader() 29 | reader.onload = function (e) { 30 | // @ts-expect-error TS(2531) FIXME: Object is possibly 'null'. 31 | props.handleData(e.target.result) 32 | } 33 | reader.readAsText(file) 34 | } 35 | return ( 36 |
37 | 40 | 46 |
47 | ) 48 | } 49 | 50 | export {FileUploadButton as default} 51 | -------------------------------------------------------------------------------- /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 | {t('noMatch.imageAlt')} 35 | 36 |

{t('noMatch.title')}

37 |

{t('noMatch.description')}

38 |

{t('noMatch.links')}

39 |
    40 |
  • 41 | {t('noMatch.home')} 42 |
  • 43 |
44 |
45 |
46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /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 | 12 | {label} 13 | 14 | {info} 15 | 16 |
{children}
17 |
18 | ) 19 | -------------------------------------------------------------------------------- /frontend/src/components/__tests__/Status.test.tsx: -------------------------------------------------------------------------------- 1 | import {formatStatus} from '../Status' 2 | 3 | describe('Given a resource status', () => { 4 | describe('when the status has a dash', () => { 5 | it('should be removed', () => { 6 | expect(formatStatus('shutting-down')).toBe('Shutting down') 7 | }) 8 | }) 9 | 10 | describe('when the status has an underscore', () => { 11 | it('should be removed', () => { 12 | expect(formatStatus('shutting_down')).toBe('Shutting down') 13 | }) 14 | }) 15 | 16 | describe('when the status is lowercase', () => { 17 | it('should be capitalized', () => { 18 | expect(formatStatus('created')).toBe('Created') 19 | }) 20 | }) 21 | 22 | describe('when the status is uppercase', () => { 23 | it('should be capitalized', () => { 24 | expect(formatStatus('CREATE_COMPLETE')).toBe('Create complete') 25 | }) 26 | }) 27 | 28 | describe('when given an undefined string', () => { 29 | it('should return an empty string', () => { 30 | expect(formatStatus()).toBe('') 31 | }) 32 | }) 33 | }) 34 | -------------------------------------------------------------------------------- /frontend/src/components/__tests__/useClusterPoll.test.tsx: -------------------------------------------------------------------------------- 1 | import {renderHook} from '@testing-library/react' 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 | 28 | beforeEach(async () => { 29 | renderResult = await waitFor(() => 30 | render( 31 | , 32 | ), 33 | ) 34 | }) 35 | 36 | it('should render the date in the expeted absolute format with provided locales and timezone', async () => { 37 | const renderedDateString = renderResult.getByTitle(dateString).textContent 38 | 39 | expect(renderedDateString).toContain('16 settembre 2022') 40 | expect(renderedDateString).toContain('14:03') 41 | expect(renderedDateString).toContain('(UTC)') 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /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/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 footerLinkProp = useMemo(() => { 30 | if (!footerLinks) { 31 | return undefined 32 | } 33 | 34 | return ( 35 |
36 |

37 | {t('helpPanel.footer.learnMore')} 38 |

39 |
    40 | {footerLinks.map(link => ( 41 |
  • 42 | {link.title} 43 |
  • 44 | ))} 45 |
46 |
47 | ) 48 | }, [footerLinks, t]) 49 | 50 | return ( 51 | {title}} footer={footerLinkProp}> 52 |
{description}
53 |
54 | ) 55 | } 56 | 57 | export default TitleDescriptionHelpPanel 58 | -------------------------------------------------------------------------------- /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/components/useLoadingState.tsx: -------------------------------------------------------------------------------- 1 | import {Box, Spinner} from '@cloudscape-design/components' 2 | import React from 'react' 3 | import {GetAppConfig, GetIdentity, GetVersion} from '../model' 4 | import {AxiosError} from 'axios' 5 | import {BoxProps} from '@cloudscape-design/components/box/interfaces' 6 | import {useState} from '../store' 7 | import {AppConfig} from '../app-config/types' 8 | import {UserIdentity} from '../auth/types' 9 | import {PCVersion} from '../types/base' 10 | 11 | const loadingSpinnerMargin: BoxProps.Spacing = {top: 'xxxl'} 12 | 13 | function LoadingSpinnerContent() { 14 | return ( 15 | 16 | 17 | 18 | ) 19 | } 20 | interface UseLoadingStateResponse { 21 | loading: boolean 22 | content: React.ReactNode 23 | } 24 | function useLoadingState( 25 | wrappedComponents: React.ReactNode, 26 | ): UseLoadingStateResponse { 27 | const identity: UserIdentity | null = useState(['identity']) 28 | const appConfig: AppConfig | null = useState(['app', 'appConfig']) 29 | const version: PCVersion | null = useState(['app', 'version', 'full']) 30 | 31 | const shouldLoadData = !identity || !appConfig || !version 32 | 33 | const [loading, setLoading] = React.useState(shouldLoadData) 34 | 35 | React.useEffect(() => { 36 | const getPreliminaryInfo = async () => { 37 | setLoading(true) 38 | await Promise.all([GetAppConfig(), GetVersion()]) 39 | 40 | try { 41 | await GetIdentity() 42 | setLoading(false) 43 | } catch (error: any) { 44 | const status = (error as AxiosError)?.response?.status 45 | if (status != 403 && status != 401) { 46 | setLoading(false) 47 | throw error // rethrow in case error is not authn/z related 48 | } 49 | } 50 | } 51 | 52 | if (shouldLoadData) { 53 | getPreliminaryInfo() 54 | } 55 | }, [shouldLoadData]) 56 | 57 | return { 58 | loading, 59 | content: loading ? : wrappedComponents, 60 | } 61 | } 62 | 63 | export {useLoadingState, LoadingSpinnerContent} 64 | -------------------------------------------------------------------------------- /frontend/src/css-properties.d.ts: -------------------------------------------------------------------------------- 1 | import 'react' 2 | 3 | declare module 'react' { 4 | interface CSSProperties { 5 | [key: `--${string}`]: string | number 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /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' 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/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 | // We must track here the features that may be under feature flagging in the wizard. 12 | export type AvailableFeature = 13 | | 'ubuntu1804' 14 | | 'fsx_ontap' 15 | | 'fsx_openzsf' 16 | | 'lustre_persistent2' 17 | | 'multiuser_cluster' 18 | | 'memory_based_scheduling' 19 | | 'slurm_accounting' 20 | | 'slurm_queue_update_strategy' 21 | | 'queues_multiple_instance_types' 22 | | 'multi_az' 23 | | 'on_node_updated' 24 | | 'dynamic_fs_mount' 25 | | 'ebs_deletion_policy' 26 | | 'efs_deletion_policy' 27 | | 'lustre_deletion_policy' 28 | | 'imds_support' 29 | | 'rhel8' 30 | | 'cost_monitoring' 31 | | 'new_resources_limits' 32 | | 'ubuntu2204' 33 | | 'login_nodes' 34 | | 'amazon_file_cache' 35 | | 'job_exclusive_allocation' 36 | | 'memory_based_scheduling_with_multiple_instance_types' 37 | | 'rhel9' 38 | -------------------------------------------------------------------------------- /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 | const region = useState(['aws', 'region']) 18 | return isFeatureEnabled(version, region, feature) 19 | } 20 | 21 | export function isFeatureEnabled( 22 | version: string, 23 | region: string, 24 | feature: AvailableFeature, 25 | ): boolean { 26 | const features = new Set(featureFlagsProvider(version, region)) 27 | const enabled = features.has(feature) 28 | if (!enabled) { 29 | console.log(`FEATURE FLAG: Feature ${feature} is disabled`) 30 | } 31 | return enabled 32 | } 33 | -------------------------------------------------------------------------------- /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 '/pcui' 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 | react: { 20 | transKeepBasicHtmlNodesFor: ['br', 'strong', 'i', 'code', 'p'], 21 | }, 22 | }) 23 | 24 | export default i18n 25 | -------------------------------------------------------------------------------- /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/Costs/__tests__/composeTimeRange.test.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 | 12 | import {composeTimeRange} from '../composeTimeRange' 13 | import tzmock from 'timezone-mock' 14 | 15 | describe('given a function to generate a time range for the last 12 months', () => { 16 | beforeEach(() => { 17 | tzmock.register('UTC') 18 | }) 19 | 20 | afterEach(() => { 21 | tzmock.unregister() 22 | }) 23 | 24 | describe('given a date', () => { 25 | let mockDate: Date 26 | 27 | beforeEach(() => { 28 | mockDate = new Date('2023-04-21T12:11:15Z') 29 | }) 30 | 31 | it('returns the given date in ISO string', () => { 32 | expect(composeTimeRange(mockDate).toDate).toBe('2023-04-21T00:00:00.000Z') 33 | }) 34 | 35 | it('returns the date of 12 months earlier in ISO string', () => { 36 | expect(composeTimeRange(mockDate).fromDate).toBe( 37 | '2022-04-21T00:00:00.000Z', 38 | ) 39 | }) 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/Costs/__tests__/valueFormatter.test.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 | 12 | import {toShortDollarAmount} from '../valueFormatter' 13 | 14 | describe('toShortDollarAmount', () => { 15 | test('returns correct value for input >= 1e9', () => { 16 | const result = toShortDollarAmount(1500000000) 17 | expect(result).toEqual('1.5G') 18 | }) 19 | 20 | test('returns correct value for input >= 1e6', () => { 21 | const result = toShortDollarAmount(2000000) 22 | expect(result).toEqual('2.0M') 23 | }) 24 | 25 | test('returns correct value for input >= 1e3', () => { 26 | const result = toShortDollarAmount(5000) 27 | expect(result).toEqual('5.0K') 28 | }) 29 | 30 | test('returns correct value for input < 1e3', () => { 31 | const result = toShortDollarAmount(123.456) 32 | expect(result).toEqual('123.46') 33 | }) 34 | 35 | test('returns correct value for negative input', () => { 36 | const result = toShortDollarAmount(-1234567890) 37 | expect(result).toEqual('-1.2G') 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/Costs/composeTimeRange.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 | function clearTimeData(date: Date) { 13 | date.setHours(0) 14 | date.setMinutes(0) 15 | date.setSeconds(0) 16 | date.setMilliseconds(0) 17 | } 18 | 19 | export function composeTimeRange(today = new Date()) { 20 | const twelveMonthsAgo = new Date(today) 21 | 22 | clearTimeData(today) 23 | clearTimeData(twelveMonthsAgo) 24 | 25 | const currentYear = today.getFullYear() 26 | 27 | twelveMonthsAgo.setFullYear(currentYear - 1) 28 | 29 | return { 30 | fromDate: twelveMonthsAgo.toISOString(), 31 | toDate: today.toISOString(), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/Costs/costs.types.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 | 12 | export type CostMonitoringStatus = boolean 13 | 14 | export interface CostMonitoringStatusResponse { 15 | active: CostMonitoringStatus 16 | } 17 | 18 | export interface CostMonitoringData { 19 | period: { 20 | start: string 21 | end: string 22 | } 23 | amount: number 24 | unit: string 25 | } 26 | 27 | export interface CostMonitoringDataResponse { 28 | costs: CostMonitoringData[] 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/Costs/index.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 {useParams} from 'react-router-dom' 13 | import {CostData} from './CostData' 14 | 15 | export function Costs() { 16 | const {clusterName} = useParams() 17 | 18 | return 19 | } 20 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/Costs/valueFormatter.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 | 12 | export function toShortDollarAmount(value: number) { 13 | const absValue = Math.abs(value) 14 | 15 | if (absValue >= 1e9) { 16 | return (value / 1e9).toFixed(1) + 'G' 17 | } 18 | 19 | if (absValue >= 1e6) { 20 | return (value / 1e6).toFixed(1) + 'M' 21 | } 22 | 23 | if (absValue >= 1e3) { 24 | return (value / 1e3).toFixed(1) + 'K' 25 | } 26 | 27 | return value.toFixed(2) 28 | } 29 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/CreateButtonDropdown/__tests__/CreateButtonDropdown.test.tsx: -------------------------------------------------------------------------------- 1 | import wrapper from '@cloudscape-design/components/test-utils/dom' 2 | import {Store} from '@reduxjs/toolkit' 3 | import {render, RenderResult} from '@testing-library/react' 4 | import i18n from 'i18next' 5 | import {mock} from 'jest-mock-extended' 6 | import {I18nextProvider, initReactI18next} from 'react-i18next' 7 | import {Provider} from 'react-redux' 8 | import {BrowserRouter} from 'react-router-dom' 9 | import {CreateButtonDropdown} from '../CreateButtonDropdown' 10 | 11 | i18n.use(initReactI18next).init({ 12 | resources: {}, 13 | lng: 'en', 14 | }) 15 | 16 | const mockStore = mock() 17 | 18 | const MockProviders = (props: any) => ( 19 | 20 | 21 | {props.children} 22 | 23 | 24 | ) 25 | 26 | describe('given a dropdown button to create a cluster', () => { 27 | let screen: RenderResult 28 | let mockOpenWizard: jest.Mock 29 | 30 | beforeEach(() => { 31 | mockOpenWizard = jest.fn() 32 | 33 | screen = render( 34 | 35 | 36 | , 37 | ) 38 | }) 39 | 40 | describe('when user selects the option to create a cluster using the wizard', () => { 41 | beforeEach(() => { 42 | const buttonDropdown = wrapper(screen.container).findButtonDropdown()! 43 | buttonDropdown.openDropdown() 44 | buttonDropdown.findItemById('wizard')?.click() 45 | }) 46 | 47 | it('should open the cluster creation wizard', () => { 48 | expect(mockOpenWizard).toHaveBeenCalledTimes(1) 49 | }) 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/FromClusterModal/useClustersToCopyFrom.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 | 12 | import {SelectProps} from '@cloudscape-design/components' 13 | import {useMemo} from 'react' 14 | import {useState} from '../../../store' 15 | import {ClusterInfoSummary, ClusterStatus} from '../../../types/clusters' 16 | 17 | function itemToOption(item: ClusterInfoSummary): SelectProps.Option { 18 | return { 19 | label: item.clusterName, 20 | value: item.clusterName, 21 | } 22 | } 23 | 24 | function matchUpToMinor(semVer1: string, semVer2: string) { 25 | const [major1, minor1] = semVer1.split('.') 26 | const [major2, minor2] = semVer2.split('.') 27 | 28 | if (major1 !== major2) return false 29 | if (minor1 !== minor2) return false 30 | 31 | return true 32 | } 33 | 34 | function canCopyFromCluster( 35 | cluster: ClusterInfoSummary, 36 | currentVersion: string, 37 | ) { 38 | if (cluster.clusterStatus === ClusterStatus.DeleteInProgress) { 39 | return false 40 | } 41 | 42 | if (!matchUpToMinor(cluster.version, currentVersion)) { 43 | return false 44 | } 45 | 46 | return true 47 | } 48 | 49 | export function useClustersToCopyFrom() { 50 | const apiVersion = useState(['app', 'version', 'full']) 51 | const clusters: ClusterInfoSummary[] = useState(['clusters', 'list']) 52 | 53 | return useMemo(() => { 54 | if (!clusters) { 55 | return [] 56 | } 57 | 58 | return clusters 59 | .filter(cluster => canCopyFromCluster(cluster, apiVersion)) 60 | .map(itemToOption) 61 | }, [apiVersion, clusters]) 62 | } 63 | -------------------------------------------------------------------------------- /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/Clusters/__tests__/util.test.ts: -------------------------------------------------------------------------------- 1 | import {selectCluster} from '../util' 2 | 3 | describe('Given a function to select the current cluster and a cluster name', () => { 4 | const clusterName = 'some-cluster-name' 5 | let mockDescribeCluster: jest.Mock 6 | let mockGetConfiguration: jest.Mock 7 | 8 | beforeEach(() => { 9 | mockDescribeCluster = jest.fn() 10 | mockGetConfiguration = jest.fn() 11 | }) 12 | 13 | describe('when user selects a cluster by name', () => { 14 | it('should describe the cluster', async () => { 15 | await selectCluster( 16 | clusterName, 17 | mockDescribeCluster, 18 | mockGetConfiguration, 19 | ) 20 | expect(mockDescribeCluster).toHaveBeenCalledTimes(1) 21 | expect(mockDescribeCluster).toHaveBeenCalledWith(clusterName) 22 | }) 23 | 24 | it('should get the cluster configuration', async () => { 25 | await selectCluster( 26 | clusterName, 27 | mockDescribeCluster, 28 | mockGetConfiguration, 29 | ) 30 | expect(mockGetConfiguration).toHaveBeenCalledTimes(1) 31 | expect(mockGetConfiguration).toHaveBeenCalledWith( 32 | clusterName, 33 | expect.any(Function), 34 | ) 35 | }) 36 | 37 | describe('when describing the cluster fails', () => { 38 | beforeEach(async () => { 39 | mockDescribeCluster = jest.fn(() => Promise.reject('any-error')) 40 | await selectCluster( 41 | clusterName, 42 | mockDescribeCluster, 43 | mockGetConfiguration, 44 | ) 45 | }) 46 | 47 | it('should not get the cluster configuration', () => { 48 | expect(mockGetConfiguration).toHaveBeenCalledTimes(0) 49 | }) 50 | }) 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Clusters/util.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 | // @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 11 | import jsyaml from 'js-yaml' 12 | 13 | import {getState, setState} from '../../store' 14 | 15 | export async function selectCluster( 16 | clusterName: string, 17 | DescribeCluster: (name: string, callback?: () => void) => Promise, 18 | GetConfiguration: (name: string, callback: (value: any) => void) => void, 19 | ) { 20 | const oldClusterName = getState(['app', 'clusters', 'selected']) 21 | let config_path = ['clusters', 'index', clusterName, 'config'] 22 | if (oldClusterName !== clusterName) { 23 | setState(['app', 'clusters', 'selected'], clusterName) 24 | } 25 | 26 | try { 27 | await DescribeCluster(clusterName) 28 | GetConfiguration(clusterName, (configuration: any) => { 29 | setState(['clusters', 'index', clusterName, 'configYaml'], configuration) 30 | setState(config_path, jsyaml.load(configuration)) 31 | }) 32 | } catch (_) { 33 | // NOOP 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Cluster/ClusterNameField.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 {FormField, Input, InputProps} from '@cloudscape-design/components' 13 | import {NonCancelableEventHandler} from '@cloudscape-design/components/internal/events' 14 | import {useCallback} from 'react' 15 | import {useTranslation} from 'react-i18next' 16 | import {setState, useState} from '../../../store' 17 | 18 | const clusterNamePath = ['app', 'wizard', 'clusterName'] 19 | const clusterNameErrorPath = [ 20 | 'app', 21 | 'wizard', 22 | 'errors', 23 | 'source', 24 | 'clusterName', 25 | ] 26 | const editingPath = ['app', 'wizard', 'editing'] 27 | 28 | export function ClusterNameField() { 29 | const {t} = useTranslation() 30 | const clusterName = useState(clusterNamePath) || '' 31 | const clusterNameError = useState(clusterNameErrorPath) 32 | const editing = !!useState(editingPath) 33 | 34 | const onChange: NonCancelableEventHandler = 35 | useCallback(({detail}) => { 36 | setState(clusterNamePath, detail.value) 37 | }, []) 38 | 39 | return ( 40 | 45 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Components.module.css: -------------------------------------------------------------------------------- 1 | .space-between-wrap { 2 | display: flex; 3 | justify-content: space-between; 4 | flex-wrap: wrap; 5 | } 6 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Components.types.ts: -------------------------------------------------------------------------------- 1 | export type Extension = { 2 | name: string 3 | path: string 4 | description: string 5 | args: {name: string; default?: string}[] 6 | } 7 | 8 | export type ActionsEditorProps = { 9 | basePath: string[] 10 | errorsPath: string[] 11 | } 12 | 13 | export type InstanceType = { 14 | type: string 15 | tags: string[] 16 | } 17 | 18 | export type InstanceGroup = InstanceType[] 19 | 20 | export type InstanceTypeOption = { 21 | label: string 22 | value: string 23 | tags: string[] 24 | } 25 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Create.types.ts: -------------------------------------------------------------------------------- 1 | type ErrorLevel = 'INFO' | 'WARNING' | 'ERROR' 2 | 3 | export type ConfigError = { 4 | id: string 5 | type: string 6 | level: ErrorLevel 7 | message: string 8 | } 9 | 10 | export type UpdateError = { 11 | parameter: string 12 | currentValue: string 13 | requestedValue: string 14 | message: string 15 | } 16 | 17 | export type CreateErrors = { 18 | message: string 19 | validationMessages?: ConfigError[] 20 | configurationValidationErrors?: ConfigError[] 21 | updateValidationErrors?: UpdateError[] 22 | } 23 | -------------------------------------------------------------------------------- /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 {useTranslation} from 'react-i18next' 6 | import {useState} from '../../../store' 7 | import {subnetName} from '../util' 8 | import {Subnet} from './queues.types' 9 | 10 | type SubnetMultiSelectProps = { 11 | value: string[] 12 | onChange: NonCancelableEventHandler 13 | } 14 | 15 | function SubnetMultiSelect({value, onChange}: SubnetMultiSelectProps) { 16 | const {t} = useTranslation() 17 | const vpc = useState(['app', 'wizard', 'vpc']) 18 | const subnets = useState(['aws', 'subnets']) 19 | const filteredSubnets: Subnet[] = useMemo( 20 | () => 21 | subnets && 22 | subnets.filter((s: Subnet) => { 23 | return vpc ? s.VpcId === vpc : true 24 | }), 25 | [subnets, vpc], 26 | ) 27 | 28 | const subnetOptions = useMemo(() => { 29 | return filteredSubnets.map((subnet: Subnet) => { 30 | return { 31 | value: subnet.SubnetId, 32 | label: subnet.SubnetId, 33 | description: 34 | subnet.AvailabilityZone + 35 | ` - ${subnet.AvailabilityZoneId}` + 36 | (subnetName(subnet) ? ` (${subnetName(subnet)})` : ''), 37 | } 38 | }) 39 | }, [filteredSubnets]) 40 | 41 | return ( 42 | { 44 | return value.includes(option.value) 45 | })} 46 | onChange={onChange} 47 | placeholder={t('wizard.queues.subnet.placeholder')} 48 | options={subnetOptions} 49 | /> 50 | ) 51 | } 52 | 53 | export {SubnetMultiSelect} 54 | -------------------------------------------------------------------------------- /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/__tests__/validateQueueName.test.ts: -------------------------------------------------------------------------------- 1 | import {validateQueueName} from '../queues.validators' 2 | 3 | describe('Given a queue name', () => { 4 | describe("when it's blank", () => { 5 | it('should fail the validation', () => { 6 | expect(validateQueueName('')).toEqual([false, 'empty']) 7 | }) 8 | }) 9 | 10 | describe("when it's more than 25 chars", () => { 11 | it('should fail the validation', () => { 12 | expect(validateQueueName(new Array(27).join('a'))).toEqual([ 13 | false, 14 | 'max_length', 15 | ]) 16 | }) 17 | }) 18 | 19 | describe("when it's less than or equal to 25 chars", () => { 20 | it('should be validated', () => { 21 | expect(validateQueueName(new Array(25).join('a'))).toEqual([true]) 22 | }) 23 | }) 24 | 25 | describe('when one or more bad characters are given', () => { 26 | it('should fail the validation', () => { 27 | expect(validateQueueName('_')).toEqual([false, 'forbidden_chars']) 28 | expect(validateQueueName('=')).toEqual([false, 'forbidden_chars']) 29 | expect(validateQueueName('A')).toEqual([false, 'forbidden_chars']) 30 | expect(validateQueueName('#')).toEqual([false, 'forbidden_chars']) 31 | }) 32 | }) 33 | 34 | describe('when no bad characters are given', () => { 35 | it('should be validated', () => { 36 | expect(validateQueueName('queue-0')).toEqual([true]) 37 | }) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/queues.types.ts: -------------------------------------------------------------------------------- 1 | export type Queue = { 2 | Name: string 3 | AllocationStrategy: AllocationStrategy 4 | ComputeResources: MultiInstanceComputeResource[] 5 | Networking?: { 6 | SubnetIds: string[] 7 | } 8 | } 9 | 10 | export type QueueValidationErrors = Record< 11 | number, 12 | 'instance_type_unique' | 'instance_types_empty' 13 | > 14 | 15 | export type ComputeResource = { 16 | Name: string 17 | MinCount: number 18 | MaxCount: number 19 | } 20 | 21 | export type SingleInstanceComputeResource = ComputeResource & { 22 | InstanceType: string 23 | } 24 | 25 | export type AllocationStrategy = 'lowest-price' | 'capacity-optimized' 26 | 27 | export type ComputeResourceInstance = {InstanceType: string} 28 | 29 | export type CapacityReservationTarget = { 30 | CapacityReservationId?: string 31 | CapacityReservationResourceGroupArn?: string 32 | } 33 | 34 | export type MultiInstanceComputeResource = ComputeResource & { 35 | Instances?: ComputeResourceInstance[] 36 | CapacityReservationTarget?: CapacityReservationTarget 37 | } 38 | 39 | export type Tag = { 40 | Key: string 41 | Value: string 42 | } 43 | 44 | export type Subnet = { 45 | SubnetId: string 46 | AvailabilityZone: string 47 | AvailabilityZoneId: string 48 | VpcId: string 49 | Tags?: Tag[] 50 | } 51 | 52 | export type ClusterResourcesLimits = { 53 | maxQueues: number 54 | maxCRPerQueue: number 55 | maxCRPerCluster: number 56 | } 57 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Queues/queues.validators.tsx: -------------------------------------------------------------------------------- 1 | export const QUEUE_NAME_MAX_LENGTH = 25 2 | export const queueNameErrorsMapping = { 3 | empty: 'wizard.queues.validation.emptyName', 4 | max_length: 'wizard.queues.validation.maxLengthName', 5 | forbidden_chars: 'wizard.queues.validation.forbiddenCharsName', 6 | } 7 | type QueueNameErrorKind = keyof typeof queueNameErrorsMapping 8 | 9 | export function validateQueueName( 10 | name: string, 11 | ): [true] | [false, QueueNameErrorKind] { 12 | if (!name) { 13 | return [false, 'empty'] 14 | } 15 | if (name.length > QUEUE_NAME_MAX_LENGTH) { 16 | return [false, 'max_length'] 17 | } 18 | if (!/^([a-z][a-z0-9-]+)$/.test(name)) { 19 | return [false, 'forbidden_chars'] 20 | } 21 | return [true] 22 | } 23 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Storage/__tests__/validateEfs.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 {EfsStorage} from '../../Storage.types' 12 | import {validateEfs} from '../storage.validators' 13 | 14 | describe('Given a function to validate an EFS storage', () => { 15 | describe('When the throughput mode is "provisioned"', () => { 16 | describe('when the provisioned throughput is NaN', () => { 17 | const efsStorage: EfsStorage = { 18 | Name: 'Efs', 19 | MountDir: '/moundDir', 20 | StorageType: 'Efs', 21 | EfsSettings: { 22 | ThroughputMode: 'provisioned', 23 | ProvisionedThroughput: NaN, 24 | }, 25 | } 26 | it('should fail the validation', () => { 27 | expect(validateEfs(efsStorage)).toEqual([ 28 | false, 29 | 'provisioned_throughput_undefined', 30 | ]) 31 | }) 32 | }) 33 | 34 | describe('when the provisioned throughput is between 1 and 1024', () => { 35 | const efsStorage: EfsStorage = { 36 | Name: 'Efs', 37 | MountDir: '/moundDir', 38 | StorageType: 'Efs', 39 | EfsSettings: { 40 | ThroughputMode: 'provisioned', 41 | ProvisionedThroughput: 256, 42 | }, 43 | } 44 | it('should be validated', () => { 45 | expect(validateEfs(efsStorage)).toEqual([true]) 46 | }) 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Storage/__tests__/validateStorageName.test.ts: -------------------------------------------------------------------------------- 1 | import {validateStorageName} from '../storage.validators' 2 | 3 | describe('Given a storage name', () => { 4 | describe("when it's more than 30 chars", () => { 5 | it('should fail the validation', () => { 6 | expect(validateStorageName(new Array(32).join('a'))).toEqual([ 7 | false, 8 | 'max_length', 9 | ]) 10 | }) 11 | }) 12 | 13 | describe("when it's less than or equal to 30 chars", () => { 14 | it('should be validated', () => { 15 | expect(validateStorageName(new Array(30).join('a'))).toEqual([true]) 16 | }) 17 | }) 18 | 19 | describe("when it's blank", () => { 20 | it('should fail the validation', () => { 21 | expect(validateStorageName('')).toEqual([false, 'empty']) 22 | }) 23 | }) 24 | 25 | describe("when the name is 'default'", () => { 26 | it('should fail the validation', () => { 27 | expect(validateStorageName('default')).toEqual([ 28 | false, 29 | 'forbidden_keyword', 30 | ]) 31 | }) 32 | }) 33 | 34 | describe('when one or more bad characters are given', () => { 35 | it('should fail the validation', () => { 36 | expect(validateStorageName(';')).toEqual([false, 'forbidden_chars']) 37 | expect(validateStorageName('``')).toEqual([false, 'forbidden_chars']) 38 | expect(validateStorageName('#')).toEqual([false, 'forbidden_chars']) 39 | }) 40 | }) 41 | 42 | describe('when no bad characters are given', () => { 43 | it('should be validated', () => { 44 | expect(validateStorageName('efs_4')).toEqual([true]) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Configure/Storage/buildStorageEntries.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 | 12 | import {canCreateStorage} from '../Storage' 13 | import {Storages, UIStorageSettings, StorageType} from '../Storage.types' 14 | 15 | export function buildStorageEntries( 16 | storages: Storages, 17 | uiStorageSettings: UIStorageSettings, 18 | selectedStorageTypes: StorageType[], 19 | ): [Storages, UIStorageSettings] { 20 | const storageEntries: Storages = [] 21 | const uiSettingsEntries: UIStorageSettings = [] 22 | 23 | const firstIndex = storages.length 24 | 25 | selectedStorageTypes.forEach((storageType: StorageType, index: number) => { 26 | const storageIndex = firstIndex + index 27 | const useExisting = !canCreateStorage( 28 | storageType, 29 | storages, 30 | uiStorageSettings, 31 | ) 32 | 33 | const storageEntry: Storages[0] = { 34 | Name: `${storageType}${storageIndex}`, 35 | StorageType: storageType, 36 | MountDir: '/shared', 37 | } 38 | const uiSettingsEntry = { 39 | useExisting, 40 | } 41 | 42 | storageEntries.push(storageEntry) 43 | uiSettingsEntries.push(uiSettingsEntry) 44 | }) 45 | 46 | return [storageEntries, uiSettingsEntries] 47 | } 48 | -------------------------------------------------------------------------------- /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/Logs/withNodeType.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 | 12 | import {Instance, NodeType} from '../../types/instances' 13 | import {LogStreamView} from '../../types/logs' 14 | 15 | function toNodeType( 16 | headNode: Instance | null, 17 | instanceId: string, 18 | ): NodeType | null { 19 | if (!headNode) return null 20 | 21 | return headNode.instanceId === instanceId 22 | ? NodeType.HeadNode 23 | : NodeType.ComputeNode 24 | } 25 | 26 | export function withNodeType( 27 | headNode: Instance | null, 28 | logStream: LogStreamView, 29 | ) { 30 | return { 31 | ...logStream, 32 | nodeType: toNodeType(headNode, logStream.instanceId), 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/old-pages/Users/__tests__/userValidate.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 {validateUser} from '../AddUserModal' 12 | 13 | describe('Given a function to validate a user email', () => { 14 | describe('when the email is not in a correct format', () => { 15 | const username = 'test-email' 16 | it('should fail the validation', () => { 17 | expect(validateUser(username)).toEqual(false) 18 | }) 19 | }) 20 | 21 | describe('when the email is missing a top-level domain', () => { 22 | const username = 'test-email@domain' 23 | it('should fail the validation', () => { 24 | expect(validateUser(username)).toEqual(false) 25 | }) 26 | }) 27 | 28 | describe('when the email is in a correct format', () => { 29 | const username = 'test-email@domain.com' 30 | it('should be validated', () => { 31 | expect(validateUser(username)).toEqual(true) 32 | }) 33 | }) 34 | }) 35 | -------------------------------------------------------------------------------- /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/shared/__tests__/extendCollectionsOptions.test.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 | 12 | import {extendCollectionsOptions} from '../extendCollectionsOptions' 13 | 14 | describe('given a mixin to extend a CollectionOptions object', () => { 15 | describe('when no options are provided', () => { 16 | it('should return the default options', () => { 17 | expect(extendCollectionsOptions()).toEqual({pagination: {pageSize: 20}}) 18 | }) 19 | }) 20 | 21 | describe('when options are provided', () => { 22 | describe('when the given options override the defaults', () => { 23 | it('should override the default options', () => { 24 | const options = {pagination: {pageSize: 15}} 25 | expect(extendCollectionsOptions(options)).toEqual({ 26 | pagination: {pageSize: 15}, 27 | }) 28 | }) 29 | }) 30 | 31 | describe('when the given options do not overlap with the defaults', () => { 32 | it('should add the defaults to the given options', () => { 33 | const options = { 34 | filtering: {defaultFilteringText: 'some-text'}, 35 | pagination: {defaultPage: 2}, 36 | } 37 | expect(extendCollectionsOptions(options)).toEqual({ 38 | filtering: {defaultFilteringText: 'some-text'}, 39 | pagination: {pageSize: 20, defaultPage: 2}, 40 | }) 41 | }) 42 | }) 43 | }) 44 | }) 45 | -------------------------------------------------------------------------------- /frontend/src/shared/extendCollectionsOptions.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 | 12 | import {UseCollectionOptions} from '@cloudscape-design/collection-hooks' 13 | 14 | const DEFAULT_USE_COLLECTION_OPTIONS = { 15 | pageSize: 20, 16 | } 17 | 18 | export function extendCollectionsOptions( 19 | options: UseCollectionOptions = {}, 20 | ): UseCollectionOptions { 21 | return { 22 | ...options, 23 | pagination: { 24 | pageSize: DEFAULT_USE_COLLECTION_OPTIONS.pageSize, 25 | ...options.pagination, 26 | }, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | import {NodeType} from './instances' 13 | 14 | export interface LogStreamsResponse { 15 | logStreams: LogStream[] 16 | } 17 | 18 | export interface LogEventsResponse { 19 | events: LogEvent[] 20 | } 21 | 22 | export type LogStreamName = string 23 | 24 | export type LogEvent = { 25 | message: string 26 | timestamp: string 27 | } 28 | 29 | export type LogEvents = LogEvent[] 30 | 31 | export type LogFilterList = LogFilterExpression[] 32 | 33 | export type LogFilterExpression = string 34 | 35 | export type LogStreams = LogStream[] 36 | 37 | export type LogStream = { 38 | // Name of the log stream. 39 | logStreamName: string 40 | // The creation time of the stream. 41 | creationTime: string 42 | // The time of the first event of the stream. 43 | firstEventTimestamp: string 44 | // 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. 45 | lastEventTimestamp: string 46 | // The last ingestion time. 47 | lastIngestionTime: string 48 | // The sequence token. 49 | uploadSequenceToken: string 50 | // The Amazon Resource Name (ARN) of the log stream. 51 | logStreamArn: string 52 | } 53 | 54 | export interface LogStreamView { 55 | logStreamName: string 56 | hostname: string 57 | instanceId: string 58 | logIdentifier: string 59 | lastEventTimestamp: string 60 | nodeType: NodeType | null 61 | } 62 | -------------------------------------------------------------------------------- /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/styled-jsx.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /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/custom-domain/custom-domain.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: AWS ParallelCluster UI - Custom Domain 3 | 4 | Parameters: 5 | CustomDomainName: 6 | Description: Custom domain name. 7 | Type: String 8 | AllowedPattern: ^(\*\.)?(((?!-)[A-Za-z0-9-]{0,62}[A-Za-z0-9])\.)+((?!-)[A-Za-z0-9-]{1,62}[A-Za-z0-9])$ 9 | MinLength: 1 10 | MaxLength: 253 11 | 12 | HostedZoneId: 13 | Description: HostedZoneId 14 | Type: AWS::Route53::HostedZone::Id 15 | 16 | Metadata: 17 | AWS::CloudFormation::Interface: 18 | ParameterGroups: 19 | - Label: 20 | default: Domain 21 | Parameters: 22 | - CustomDomainName 23 | - Label: 24 | default: Networking 25 | Parameters: 26 | - HostedZoneId 27 | 28 | Resources: 29 | CustomDomainCertificate: 30 | Type: AWS::CertificateManager::Certificate 31 | Properties: 32 | DomainName: !Ref CustomDomainName 33 | DomainValidationOptions: 34 | - DomainName: !Ref CustomDomainName 35 | HostedZoneId: !Ref HostedZoneId 36 | KeyAlgorithm: RSA_2048 37 | SubjectAlternativeNames: 38 | - !Sub "*.${CustomDomainName}" 39 | ValidationMethod: DNS 40 | 41 | Outputs: 42 | CustomDomainName: 43 | Value: !Ref CustomDomainName 44 | Description: Custom domain name. 45 | CustomDomainCertificate: 46 | Value: !Ref CustomDomainCertificate 47 | Description: ACM certificate to certify the custom domain name and its subdomains. 48 | -------------------------------------------------------------------------------- /infrastructure/environments/demo-cfn-update-args.yaml: -------------------------------------------------------------------------------- 1 | TemplateURL: BUCKET_URL_PLACEHOLDER/parallelcluster-ui.yaml 2 | Parameters: 3 | - ParameterKey: AdminUserEmail 4 | UsePreviousValue: true 5 | - ParameterKey: Version 6 | ParameterValue: 3.12.0 7 | - ParameterKey: InfrastructureBucket 8 | ParameterValue: BUCKET_URL_PLACEHOLDER 9 | - ParameterKey: PublicEcrImageUri 10 | ParameterValue: public.ecr.aws/pcm/parallelcluster-ui:latest 11 | # Use the value below if you want to deploy the local image of PCUI. 12 | # ParameterValue: ACCOUNT_ID.dkr.ecr.REGION.amazonaws.com/parallelcluster-ui:latest 13 | - ParameterKey: UserPoolId 14 | UsePreviousValue: true 15 | - ParameterKey: UserPoolAuthDomain 16 | UsePreviousValue: true 17 | - ParameterKey: SNSRole 18 | UsePreviousValue: true 19 | - ParameterKey: ImageBuilderVpcId 20 | UsePreviousValue: true 21 | - ParameterKey: ImageBuilderSubnetId 22 | UsePreviousValue: true 23 | - ParameterKey: VpcEndpointId 24 | UsePreviousValue: true 25 | - ParameterKey: LambdaSubnetIds 26 | UsePreviousValue: true 27 | - ParameterKey: LambdaSecurityGroupIds 28 | UsePreviousValue: true 29 | - ParameterKey: AdditionalPoliciesPCAPI 30 | UsePreviousValue: true 31 | - ParameterKey: PermissionsBoundaryPolicy 32 | UsePreviousValue: true 33 | - ParameterKey: PermissionsBoundaryPolicyPCAPI 34 | UsePreviousValue: true 35 | - ParameterKey: IAMRoleAndPolicyPrefix 36 | UsePreviousValue: true 37 | - ParameterKey: CustomDomain 38 | UsePreviousValue: true 39 | - ParameterKey: CustomDomainCertificateArn 40 | UsePreviousValue: true 41 | - ParameterKey: CognitoCustomDomain 42 | UsePreviousValue: true 43 | - ParameterKey: CognitoCustomDomainCertificateArn 44 | UsePreviousValue: true 45 | Capabilities: 46 | - CAPABILITY_AUTO_EXPAND 47 | - CAPABILITY_NAMED_IAM 48 | DisableRollback: false 49 | -------------------------------------------------------------------------------- /infrastructure/environments/demo-variables.sh: -------------------------------------------------------------------------------- 1 | REGION=eu-west-1 2 | STACK_NAME=parallelcluster-ui-demo 3 | INFRA_BUCKET_STACK_NAME=pcluster-manager-github 4 | # If the bucket used by the environment is not managed by a dedicated CloudFormation stack, 5 | # specify the bucket name as follows and comment out 'INFRA_BUCKET_STACK_NAME'. 6 | # INFRA_BUCKET_NAME=pcluster-ui-bucket -------------------------------------------------------------------------------- /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/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=(parallelcluster-ui-cognito.yaml parallelcluster-ui.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}/parallelcluster-ui.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/aws-parallelcluster-ui/5703ff906f67a7ddf6c30e74a7b52f41091ef98c/pcluster_logo.png -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = . -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | pip-tools==6.13.0 -------------------------------------------------------------------------------- /requirements.in: -------------------------------------------------------------------------------- 1 | Flask-Cors==4.0.2 2 | Flask==2.3.3 3 | Werkzeug==3.0.6 4 | boto3==1.24.30 5 | requests==2.32.0 6 | urllib3==1.26.19 7 | # Installing cryptography backend since it is the recommended one: https://pypi.org/project/python-jose/ 8 | python-jose[cryptography]==3.4.0 9 | cryptography==44.0.1 10 | PyYAML==6.0.2 11 | pytest==7.2.2 12 | pytest-mock==3.8.2 13 | itsdangerous==2.1.2 14 | marshmallow==3.19.0 15 | certifi==2024.7.4 16 | jinja2==3.1.6 -------------------------------------------------------------------------------- /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/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/common.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function info() { 4 | echo "[INFO] $1" 5 | } 6 | 7 | function warn() { 8 | echo "[WARN] $1" 9 | } 10 | 11 | function fail() { 12 | echo "[ERROR] $1" && exit 1 13 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /scripts/tail-logs.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 | # This script is used to update the infrastructure of a given PCM environment. 15 | # An environment is composed of a list of variables with the entrypoints of the environment 16 | # and a CloudFormation request file where the stack update can be customized, 17 | # for example by changing the parameters provided to the previous version of the stack 18 | # 19 | # Usage: ./scripts/tail.sh [ENVIRONMENT] 20 | # Example: ./scripts/tail.sh demo 21 | 22 | CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd)" 23 | 24 | INFRASTRUCTURE_DIR="$CURRENT_DIR/../infrastructure" 25 | 26 | source "$CURRENT_DIR/common.sh" 27 | 28 | ENVIRONMENT=$1 29 | 30 | [[ -z $ENVIRONMENT ]] && fail "Missing required argument: ENVIRONMENT" 31 | 32 | info "Selected environment: $ENVIRONMENT" 33 | 34 | source "$INFRASTRUCTURE_DIR/environments/$ENVIRONMENT-variables.sh" 35 | 36 | info "Retrieving log group" 37 | LOG_GROUP=$(aws cloudformation describe-stack-resources \ 38 | --region "$REGION" \ 39 | --stack-name "$STACK_NAME" \ 40 | --logical-resource-id ParallelClusterUILambdaLogGroup \ 41 | --output text \ 42 | --query 'StackResources[0].PhysicalResourceId') 43 | 44 | aws logs tail $LOG_GROUP --region $REGION --follow --format short -------------------------------------------------------------------------------- /uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | module = app 3 | callable = app 4 | master = true 5 | --------------------------------------------------------------------------------