From d786249cf39f8f7a414e68e0e08c9b5992efdf76 Mon Sep 17 00:00:00 2001 From: Fede Alonso Date: Fri, 6 Sep 2024 15:27:32 +0200 Subject: [PATCH] Test: Import and run a Pipeline (#3028) * Provision project and data connection, clean up project * fix temporal path * add DSPA Secret provision * Provision and clean-up working properly * Import a pipeline from DSP modal * Test running OK * clean comments and disable the fetching of backend requests in live testing * fix lintings * Fixes to 2.12 compatibility * Delete finalizers, generation and managedFields from yaml * Update frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts Co-authored-by: Christian Vogt * some fixes and run pipeline from actions * delete wait when waiting run status * fix oc commands * delete login assertion * stop waiting for project deletion * AWS like ods-ci * lint fixes * modularize the oc calls * lint fixes * delete comment * Use a dummy pipeline from our repo * test: Add a dummy pipeline sample * add dummy pipeline * fix pipeline url * filter the project before trying to open it in the DSP view * fix data-testid for pipeline run status icon * yml to yaml * delete unused code * filter project name properly * Wait for status editable * make display name not mandatory * move filterProjectByName * use proper types * lint fixes * add lint fixes * Update frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts Co-authored-by: Christian Vogt * Update frontend/src/__tests__/cypress/cypress/utils/oc_commands/dspa.ts Co-authored-by: Christian Vogt * delete displayName redundancy * Update frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts Co-authored-by: Christian Vogt * Update frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts Co-authored-by: Christian Vogt * Update frontend/src/__tests__/cypress/cypress/pages/projects.ts Co-authored-by: Christian Vogt * move uncaught exception to particular test * delete the uncaught exception control as it's fixed * Update frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts Co-authored-by: Christian Vogt * Update frontend/src/__tests__/cypress/cypress/utils/oc_commands/baseCommands.ts Co-authored-by: Christian Vogt * add log when deleting a project using oc * add a helper which contains the test provisioning * lint fixes --------- Co-authored-by: Christian Vogt --- frontend/src/__tests__/cypress/.gitignore | 2 + .../resources/yaml/data_connection.yaml | 18 +++++ .../cypress/fixtures/resources/yaml/dspa.yaml | 49 ++++++++++++ .../fixtures/resources/yaml/dspa_secret.yaml | 11 +++ .../pages/pipelines/pipelinesGlobal.ts | 8 +- .../cypress/pages/pipelines/topology.ts | 12 ++- .../cypress/cypress/pages/projects.ts | 14 ++++ .../__tests__/cypress/cypress/support/e2e.ts | 5 ++ .../tests/e2e/pipelines/pipelines.cy.ts | 76 +++++++++++++++++++ .../src/__tests__/cypress/cypress/types.ts | 41 ++++++++++ .../cypress/utils/oc_commands/baseCommands.ts | 21 +++++ .../utils/oc_commands/dataConnection.ts | 30 ++++++++ .../cypress/cypress/utils/oc_commands/dspa.ts | 50 ++++++++++++ .../cypress/utils/oc_commands/project.ts | 48 ++++++++++++ .../cypress/cypress/utils/pipelines.ts | 50 ++++++++++++ .../cypress/cypress/utils/s3Buckets.ts | 3 + .../cypress/cypress/utils/testConfig.ts | 21 ++++- .../cypress/cypress/utils/yaml_files.ts | 18 +++++ .../cypress/test-variables.yml.example | 9 ++- .../pipelines_samples/dummy_pipeline.py | 19 +++++ .../dummy_pipeline_compiled.yaml | 61 +++++++++++++++ .../pipelinesDetails/PipelineDetailsTitle.tsx | 2 +- 22 files changed, 562 insertions(+), 6 deletions(-) create mode 100644 frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/data_connection.yaml create mode 100644 frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa.yaml create mode 100644 frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa_secret.yaml create mode 100644 frontend/src/__tests__/cypress/cypress/tests/e2e/pipelines/pipelines.cy.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/oc_commands/baseCommands.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/oc_commands/dataConnection.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/oc_commands/dspa.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/pipelines.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/s3Buckets.ts create mode 100644 frontend/src/__tests__/cypress/cypress/utils/yaml_files.ts create mode 100644 frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline.py create mode 100644 frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline_compiled.yaml diff --git a/frontend/src/__tests__/cypress/.gitignore b/frontend/src/__tests__/cypress/.gitignore index 21afa5e2e9..7ecf5c532b 100644 --- a/frontend/src/__tests__/cypress/.gitignore +++ b/frontend/src/__tests__/cypress/.gitignore @@ -2,3 +2,5 @@ cypress/downloads coverage results .nyc_output +test-variables.yml +cypress/temp*.yaml \ No newline at end of file diff --git a/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/data_connection.yaml b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/data_connection.yaml new file mode 100644 index 0000000000..3e82f2a930 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/data_connection.yaml @@ -0,0 +1,18 @@ +kind: Secret +apiVersion: v1 +metadata: + name: aws-connection-ods-ci-ds-pipelines + namespace: {{NAMESPACE}} + labels: + opendatahub.io/dashboard: 'true' + opendatahub.io/managed: 'true' + annotations: + opendatahub.io/connection-type: s3 + openshift.io/display-name: ods-ci-ds-pipelines +data: + AWS_ACCESS_KEY_ID: {{AWS_ACCESS_KEY_ID}} + AWS_DEFAULT_REGION: {{AWS_DEFAULT_REGION}} + AWS_S3_BUCKET: {{AWS_S3_BUCKET}} + AWS_S3_ENDPOINT: {{AWS_S3_ENDPOINT}} + AWS_SECRET_ACCESS_KEY: {{AWS_SECRET_ACCESS_KEY}} +type: Opaque diff --git a/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa.yaml b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa.yaml new file mode 100644 index 0000000000..3f2c702c5d --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa.yaml @@ -0,0 +1,49 @@ +apiVersion: datasciencepipelinesapplications.opendatahub.io/v1alpha1 +kind: DataSciencePipelinesApplication +metadata: + name: dspa + namespace: {{NAMESPACE}} +spec: + apiServer: + caBundleFileMountPath: '' + stripEOF: true + dbConfigConMaxLifetimeSec: 120 + applyTektonCustomResource: true + caBundleFileName: '' + deploy: true + enableSamplePipeline: false + autoUpdatePipelineDefaultVersion: true + archiveLogs: false + terminateStatus: Cancelled + enableOauth: true + trackArtifacts: true + collectMetrics: true + injectDefaultScript: true + database: + disableHealthCheck: false + mariaDB: + deploy: true + pipelineDBName: mlpipeline + pvcSize: 10Gi + username: mlpipeline + dspVersion: v2 + objectStorage: + disableHealthCheck: false + enableExternalRoute: false + externalStorage: + basePath: '' + bucket: {{AWS_S3_BUCKET}} + host: s3.amazonaws.com + port: '' + region: us-east-1 + s3CredentialsSecret: + accessKey: AWS_ACCESS_KEY_ID + secretKey: AWS_SECRET_ACCESS_KEY + secretName: {{DSPA_SECRET_NAME}} + scheme: https + persistenceAgent: + deploy: true + numWorkers: 2 + scheduledWorkflow: + cronScheduleTimezone: UTC + deploy: true diff --git a/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa_secret.yaml b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa_secret.yaml new file mode 100644 index 0000000000..e29067e205 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/fixtures/resources/yaml/dspa_secret.yaml @@ -0,0 +1,11 @@ +kind: Secret +apiVersion: v1 +metadata: + name: {{DSPA_SECRET_NAME}} + namespace: {{NAMESPACE}} + labels: + opendatahub.io/dashboard: 'true' +data: + AWS_ACCESS_KEY_ID: {{AWS_ACCESS_KEY_ID}} + AWS_SECRET_ACCESS_KEY: {{AWS_SECRET_ACCESS_KEY}} +type: Opaque diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts index a0f208d413..3089afe1a3 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/pipelinesGlobal.ts @@ -1,5 +1,6 @@ import { DeleteModal } from '~/__tests__/cypress/cypress/pages/components/DeleteModal'; import { Modal } from '~/__tests__/cypress/cypress/pages/components/Modal'; +import { appChrome } from '~/__tests__/cypress/cypress/pages/appChrome'; class PipelinesGlobal { visit(projectName: string) { @@ -7,6 +8,11 @@ class PipelinesGlobal { this.wait(); } + navigate() { + appChrome.findNavItem('Data Science Pipelines').click(); + this.wait(); + } + private wait() { cy.findByTestId('app-page-title').contains('Pipelines'); cy.testA11y(); @@ -42,7 +48,7 @@ class PipelinesGlobal { return cy.findByRole('menuitem').get('span').contains('Upload new version'); } - private findProjectSelect() { + findProjectSelect() { return cy.findByTestId('project-selector-dropdown'); } diff --git a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts index 28d0a42d46..5035cb4b13 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/pipelines/topology.ts @@ -122,6 +122,14 @@ class RunDetails extends PipelinesTopology { return new DetailsItem(() => cy.findByTestId(`detail-item-${key}`)); } + private findStatusLabel(timeout?: number) { + return cy.findByTestId('status-icon', { timeout }); + } + + expectStatusLabelToBe(statusValue: string, timeout?: number) { + this.findStatusLabel(timeout).should('have.text', statusValue); + } + findRightDrawer() { return new PipelineRunRightDrawer(() => cy.findByTestId('pipeline-run-drawer-right-content').parent(), @@ -169,8 +177,8 @@ class PipelineDetails extends PipelinesTopology { return new DashboardCodeEditor(() => cy.findByTestId('pipeline-dashboard-code-editor')); } - findPageTitle() { - return cy.findByTestId('app-page-title'); + findPageTitle(timeout?: number) { + return cy.findByTestId('app-page-title', { timeout }); } getTaskDrawer() { diff --git a/frontend/src/__tests__/cypress/cypress/pages/projects.ts b/frontend/src/__tests__/cypress/cypress/pages/projects.ts index 3bcdad54a3..4e3ecfe04d 100644 --- a/frontend/src/__tests__/cypress/cypress/pages/projects.ts +++ b/frontend/src/__tests__/cypress/cypress/pages/projects.ts @@ -95,6 +95,16 @@ class ProjectListPage { findCreateWorkbenchButton() { return cy.findByRole('button', { name: 'Create a workbench' }); } + + /** + * Filter Project by name using the Project filter from the Data Science Projects view + * @param projectName Project Name + */ + filterProjectByName = (projectName: string) => { + const projectListToolbar = projectListPage.getTableToolbar(); + projectListToolbar.findFilterMenuOption('filter-dropdown-select', 'Name').click(); + projectListToolbar.findSearchInput().type(projectName); + }; } class CreateEditProjectModal extends Modal { @@ -159,6 +169,10 @@ class ProjectDetails { ); } + findImportPipelineButton(timeout?: undefined) { + return cy.findByTestId('import-pipeline-button', { timeout }); + } + findSingleModelDeployButton() { return this.findModelServingPlatform('single').findByTestId('single-serving-deploy-button'); } diff --git a/frontend/src/__tests__/cypress/cypress/support/e2e.ts b/frontend/src/__tests__/cypress/cypress/support/e2e.ts index 121f7948f2..392e6dc457 100644 --- a/frontend/src/__tests__/cypress/cypress/support/e2e.ts +++ b/frontend/src/__tests__/cypress/cypress/support/e2e.ts @@ -28,6 +28,11 @@ Cypress.Keyboard.defaults({ keystrokeDelay: 0, }); +before(() => { + // disable Cypress's default behavior of logging all XMLHttpRequests and fetches + cy.intercept({ resourceType: /xhr|fetch/ }, { log: false }); +}); + beforeEach(() => { if (Cypress.env('MOCK')) { // fallback: return 404 for all api requests diff --git a/frontend/src/__tests__/cypress/cypress/tests/e2e/pipelines/pipelines.cy.ts b/frontend/src/__tests__/cypress/cypress/tests/e2e/pipelines/pipelines.cy.ts new file mode 100644 index 0000000000..221b76c60f --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/tests/e2e/pipelines/pipelines.cy.ts @@ -0,0 +1,76 @@ +import { deleteOpenShiftProject } from '~/__tests__/cypress/cypress/utils/oc_commands/project'; +import { ADMIN_USER } from '~/__tests__/cypress/cypress/utils/e2eUsers'; +import { projectListPage, projectDetails } from '~/__tests__/cypress/cypress/pages/projects'; +import { pipelineImportModal } from '~/__tests__/cypress/cypress/pages/pipelines/pipelineImportModal'; +import { createRunPage } from '~/__tests__/cypress/cypress/pages/pipelines/createRunPage'; +import { + pipelineDetails, + pipelineRunDetails, +} from '~/__tests__/cypress/cypress/pages/pipelines/topology'; +import { provisionProjectForPipelines } from '~/__tests__/cypress/cypress/utils/pipelines'; + +const projectName = 'test-pipelines-prj'; +const dspaSecretName = 'dashboard-dspa-secret'; +const testPipelineName = 'test-pipelines-pipeline'; +const testRunName = 'test-pipelines-run'; + +describe('An admin user can import and run a pipeline', { testIsolation: false }, () => { + before(() => { + // Create a Project for pipelines + provisionProjectForPipelines(projectName, dspaSecretName); + }); + + after(() => { + // Delete provisioned Project + deleteOpenShiftProject(projectName); + }); + + it('An admin User can Import and Run a Pipeline', () => { + // Login as an admin + cy.visitWithLogin('/', ADMIN_USER); + + /** + * Import Pipeline by URL from Project Details view + */ + projectListPage.navigate(); + + // Open the project + projectListPage.filterProjectByName(projectName); + projectListPage.findProjectLink(projectName).click(); + + // Increasing the timeout to ~3mins so the DSPA can be loaded + projectDetails.findImportPipelineButton(180000).click(); + + // Fill tue Import Pipeline modal + pipelineImportModal.findPipelineNameInput().type(testPipelineName); + pipelineImportModal.findPipelineDescriptionInput().type('Pipeline Description'); + pipelineImportModal.findImportPipelineRadio().click(); + pipelineImportModal + .findPipelineUrlInput() + //TODO: modify this URL once the PR is merged + .type( + 'https://raw.githubusercontent.com/opendatahub-io/odh-dashboard/caab82536b4dd5d39fb7a06a6c3248f10c183417/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline_compiled.yaml', + ); + pipelineImportModal.submit(); + + // Verify that we are at the details page of the pipeline by checking the title + // It can take a little longer to load + pipelineDetails.findPageTitle(60000).should('have.text', testPipelineName); + + /** + * Run the Pipeline using the Actions button in the pipeline detail view + */ + + pipelineDetails.selectActionDropdownItem('Create run'); + + //Fill the Create run fields + createRunPage.findExperimentSelect().click(); + createRunPage.selectExperimentByName('Default'); + createRunPage.fillName(testRunName); + createRunPage.fillDescription('Run Description'); + createRunPage.findSubmitButton().click(); + + //Redirected to the Graph view of the created run + pipelineRunDetails.expectStatusLabelToBe('Succeeded', 180000); + }); +}); diff --git a/frontend/src/__tests__/cypress/cypress/types.ts b/frontend/src/__tests__/cypress/cypress/types.ts index 1dfc642cf7..6b7297b9aa 100644 --- a/frontend/src/__tests__/cypress/cypress/types.ts +++ b/frontend/src/__tests__/cypress/cypress/types.ts @@ -25,8 +25,49 @@ export type UserAuthConfig = { PASSWORD: string; }; +export type AWSS3BucketDetails = { + NAME: string; + REGION: string; + ENDPOINT: string; +}; + +export type AWSS3Buckets = { + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; + BUCKET_2: AWSS3BucketDetails; +}; + +export type DataConnectionReplacements = { + NAMESPACE: string; + AWS_ACCESS_KEY_ID: string; + AWS_DEFAULT_REGION: string; + AWS_S3_BUCKET: string; + AWS_S3_ENDPOINT: string; + AWS_SECRET_ACCESS_KEY: string; +}; + +export type DspaSecretReplacements = { + DSPA_SECRET_NAME: string; + NAMESPACE: string; + AWS_ACCESS_KEY_ID: string; + AWS_SECRET_ACCESS_KEY: string; +}; + +export type DspaReplacements = { + DSPA_SECRET_NAME: string; + NAMESPACE: string; + AWS_S3_BUCKET: string; +}; + +export type CommandLineResult = { + code: number; + stdout: string; + stderr: string; +}; + export type TestConfig = { ODH_DASHBOARD_URL: string; TEST_USER: UserAuthConfig; OCP_ADMIN_USER: UserAuthConfig; + S3: AWSS3Buckets; }; diff --git a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/baseCommands.ts b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/baseCommands.ts new file mode 100644 index 0000000000..9677e993cc --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/baseCommands.ts @@ -0,0 +1,21 @@ +import type { CommandLineResult } from '~/__tests__/cypress/cypress/types'; + +/** + * Applies the given YAML content using the `oc apply` command. + * + * @param yamlContent YAML content to be applied + * @returns Cypress Chainable + */ +export const applyOpenShiftYaml = (yamlContent: string): Cypress.Chainable => { + const ocCommand = `oc apply -f - < { + if (result.code !== 0) { + // If there is an error, log the error and fail the test + cy.log(`ERROR applying YAML content + stdout: ${result.stdout} + stderr: ${result.stderr}`); + throw new Error(`Command failed with code ${result.code}`); + } + return result; + }); +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dataConnection.ts b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dataConnection.ts new file mode 100644 index 0000000000..592e63e1cb --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dataConnection.ts @@ -0,0 +1,30 @@ +import type { + DataConnectionReplacements, + CommandLineResult, +} from '~/__tests__/cypress/cypress/types'; +import { replacePlaceholdersInYaml } from '~/__tests__/cypress/cypress/utils/yaml_files'; +import { applyOpenShiftYaml } from './baseCommands'; + +/** + * Try to create a data connection based on the dataConnectionReplacements config + * @param dataConnectionReplacements Dictionary with the config values + * Dict Structure: + * dataConnectionReplacements = { + * NAMESPACE: , + * AWS_ACCESS_KEY_ID: , + * AWS_DEFAULT_REGION: , + * AWS_S3_BUCKET: , + * AWS_S3_ENDPOINT: , + * AWS_SECRET_ACCESS_KEY: , + * } + * @param yamlFilePath + */ +export const createDataConnection = ( + dataConnectionReplacements: DataConnectionReplacements, + yamlFilePath = 'resources/yaml/data_connection.yaml', +): Cypress.Chainable => { + return cy.fixture(yamlFilePath).then((yamlContent) => { + const modifiedYamlContent = replacePlaceholdersInYaml(yamlContent, dataConnectionReplacements); + return applyOpenShiftYaml(modifiedYamlContent); + }); +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dspa.ts b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dspa.ts new file mode 100644 index 0000000000..5eaaf5382a --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/dspa.ts @@ -0,0 +1,50 @@ +import { replacePlaceholdersInYaml } from '~/__tests__/cypress/cypress/utils/yaml_files'; +import type { + DspaSecretReplacements, + DspaReplacements, + CommandLineResult, +} from '~/__tests__/cypress/cypress/types'; +import { applyOpenShiftYaml } from './baseCommands'; + +/** + * Try to create a DSPA Secret based on the dspaSecretReplacements config + * @param dspaSecretReplacements Dictionary with the config values + * Dict Structure: + * dspaSecretReplacements = { + * DSPA_SECRET_NAME: , + * NAMESPACE: , + * AWS_ACCESS_KEY_ID: , + * AWS_SECRET_ACCESS_KEY: , + * } + * @param yamlFilePath + */ +export const createDSPASecret = ( + dspaSecretReplacements: DspaSecretReplacements, + yamlFilePath = 'resources/yaml/dspa_secret.yaml', +): Cypress.Chainable => { + return cy.fixture(yamlFilePath).then((yamlContent) => { + const modifiedYamlContent = replacePlaceholdersInYaml(yamlContent, dspaSecretReplacements); + return applyOpenShiftYaml(modifiedYamlContent); + }); +}; + +/** + * Try to create a DSPA based on the dspaReplacements config + * @param dspaReplacements Dictionary with the config values + * Dict Structure: + * dspaSecretReplacements = { + * DSPA_SECRET_NAME: , + * NAMESPACE: , + * AWS_S3_BUCKET: + * } + * @param yamlFilePath + */ +export const createDSPA = ( + dspaReplacements: DspaReplacements, + yamlFilePath = 'resources/yaml/dspa.yaml', +): Cypress.Chainable => { + return cy.fixture(yamlFilePath).then((yamlContent) => { + const modifiedYamlContent = replacePlaceholdersInYaml(yamlContent, dspaReplacements); + return applyOpenShiftYaml(modifiedYamlContent); + }); +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts new file mode 100644 index 0000000000..a56daf6b3d --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/oc_commands/project.ts @@ -0,0 +1,48 @@ +import type { CommandLineResult } from '~/__tests__/cypress/cypress/types'; + +/** + * Create an Openshift Project + * + * @param projectName Project Name + * @param displayName Project Display Name + * @returns Result Object of the operation + */ +export const createOpenShiftProject = ( + projectName: string, + displayName?: string, +): Cypress.Chainable => { + const ocCommand = displayName + ? `oc new-project ${projectName} --display-name='${displayName}'` + : `oc new-project ${projectName}`; + + return cy.exec(ocCommand, { failOnNonZeroExit: false }).then((result) => { + if (result.code !== 0) { + cy.log(`ERROR provisioning ${projectName} Project + stdout: ${result.stdout} + stderr: ${result.stderr}`); + throw new Error(`Command failed with code ${result.code}`); + } + return result; + }); +}; + +/** + * Delete an Openshift Project given its name + * + * @param projectName OpenShift Project name + * @returns Result Object of the operation + */ +export const deleteOpenShiftProject = ( + projectName: string, +): Cypress.Chainable => { + const ocCommand = `oc delete project ${projectName}`; + return cy.exec(ocCommand, { failOnNonZeroExit: false }).then((result) => { + if (result.code !== 0) { + cy.log(`ERROR deleting ${projectName} Project + stdout: ${result.stdout} + stderr: ${result.stderr}`); + throw new Error(`Command failed with code ${result.code}`); + } + return result; + }); +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/pipelines.ts b/frontend/src/__tests__/cypress/cypress/utils/pipelines.ts new file mode 100644 index 0000000000..21b268edb9 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/pipelines.ts @@ -0,0 +1,50 @@ +// Import necessary functions and types +import { createOpenShiftProject } from '~/__tests__/cypress/cypress/utils/oc_commands/project'; +import { createDataConnection } from '~/__tests__/cypress/cypress/utils/oc_commands/dataConnection'; +import { createDSPASecret, createDSPA } from '~/__tests__/cypress/cypress/utils/oc_commands/dspa'; +import { AWS_BUCKETS } from '~/__tests__/cypress/cypress/utils/s3Buckets'; +import type { + DataConnectionReplacements, + DspaSecretReplacements, + DspaReplacements, +} from '~/__tests__/cypress/cypress/types'; + +/** + * Provision (using oc) a Project in order to make it usable with pipelines + * (creates a Data Connection, a DSPA Secret and a DSPA) + * + * @param projectName Project Name + * @param dspaSecretName DSPA Secret Name + */ +export const provisionProjectForPipelines = (projectName: string, dspaSecretName: string): void => { + // Provision a Project + createOpenShiftProject(projectName); + + // Create a pipeline compatible Data Connection + const dataConnectionReplacements: DataConnectionReplacements = { + NAMESPACE: projectName, + AWS_ACCESS_KEY_ID: Buffer.from(AWS_BUCKETS.AWS_ACCESS_KEY_ID).toString('base64'), + AWS_DEFAULT_REGION: Buffer.from(AWS_BUCKETS.BUCKET_2.REGION).toString('base64'), + AWS_S3_BUCKET: Buffer.from(AWS_BUCKETS.BUCKET_2.NAME).toString('base64'), + AWS_S3_ENDPOINT: Buffer.from(AWS_BUCKETS.BUCKET_2.ENDPOINT).toString('base64'), + AWS_SECRET_ACCESS_KEY: Buffer.from(AWS_BUCKETS.AWS_SECRET_ACCESS_KEY).toString('base64'), + }; + createDataConnection(dataConnectionReplacements); + + // Configure Pipeline server: Create DSPA Secret + const dspaSecretReplacements: DspaSecretReplacements = { + DSPA_SECRET_NAME: dspaSecretName, + NAMESPACE: projectName, + AWS_ACCESS_KEY_ID: Buffer.from(AWS_BUCKETS.AWS_ACCESS_KEY_ID).toString('base64'), + AWS_SECRET_ACCESS_KEY: Buffer.from(AWS_BUCKETS.AWS_SECRET_ACCESS_KEY).toString('base64'), + }; + createDSPASecret(dspaSecretReplacements); + + // Configure Pipeline server: Create DSPA + const dspaReplacements: DspaReplacements = { + DSPA_SECRET_NAME: dspaSecretName, + NAMESPACE: projectName, + AWS_S3_BUCKET: AWS_BUCKETS.BUCKET_2.NAME, + }; + createDSPA(dspaReplacements); +}; diff --git a/frontend/src/__tests__/cypress/cypress/utils/s3Buckets.ts b/frontend/src/__tests__/cypress/cypress/utils/s3Buckets.ts new file mode 100644 index 0000000000..ab2638b210 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/s3Buckets.ts @@ -0,0 +1,3 @@ +import type { AWSS3Buckets } from '~/__tests__/cypress/cypress/types'; + +export const AWS_BUCKETS: AWSS3Buckets = Cypress.env('AWS_PIPELINES'); diff --git a/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts b/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts index 13d4bffb38..4efb321c05 100644 --- a/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts +++ b/frontend/src/__tests__/cypress/cypress/utils/testConfig.ts @@ -3,7 +3,12 @@ import path from 'path'; import { env } from 'process'; import dotenv from 'dotenv'; import YAML from 'yaml'; -import type { UserAuthConfig, TestConfig } from '~/__tests__/cypress/cypress/types'; +import type { + UserAuthConfig, + TestConfig, + AWSS3BucketDetails, + AWSS3Buckets, +} from '~/__tests__/cypress/cypress/types'; [ `.env.cypress${env.CY_MOCK ? '.mock' : ''}.local`, @@ -34,10 +39,24 @@ const ADMIN_USER: UserAuthConfig = testConfig?.OCP_ADMIN_USER ?? { PASSWORD: env.ADMIN_USER_PASSWORD || '', }; +const AWS_PIPELINES_BUCKET_DETAILS: AWSS3BucketDetails = { + NAME: testConfig?.S3.BUCKET_2.NAME || env.AWS_PIPELINES_BUCKET_NAME || '', + REGION: testConfig?.S3.BUCKET_2.REGION || env.AWS_PIPELINES_BUCKET_REGION || '', + ENDPOINT: testConfig?.S3.BUCKET_2.ENDPOINT || env.AWS_PIPELINES_BUCKET_ENDPOINT || '', +}; +const AWS_PIPELINES: AWSS3Buckets = { + AWS_ACCESS_KEY_ID: + testConfig?.S3.AWS_ACCESS_KEY_ID || env.AWS_PIPELINES_BUCKET_ACCESS_KEY_ID || '', + AWS_SECRET_ACCESS_KEY: + testConfig?.S3.AWS_SECRET_ACCESS_KEY || env.AWS_PIPELINES_BUCKET_SECRET_ACCESS_KEY || '', + BUCKET_2: AWS_PIPELINES_BUCKET_DETAILS, +}; + // spread the cypressEnv variables into the cypress config export const cypressEnv = { TEST_USER, ADMIN_USER, + AWS_PIPELINES, }; // re-export the updated process env diff --git a/frontend/src/__tests__/cypress/cypress/utils/yaml_files.ts b/frontend/src/__tests__/cypress/cypress/utils/yaml_files.ts new file mode 100644 index 0000000000..f55c505aa6 --- /dev/null +++ b/frontend/src/__tests__/cypress/cypress/utils/yaml_files.ts @@ -0,0 +1,18 @@ +/** + * Replaces placeholders in a YAML content with environment variable values. + * + * @param yamlContent YAML content as a string + * @param replacements Object containing placeholder keys and their corresponding replacement values + * @returns Modified YAML content + */ +export const replacePlaceholdersInYaml = ( + yamlContent: string, + replacements: { [key: string]: string }, +): string => { + let modifiedYaml = yamlContent; + for (const [key, value] of Object.entries(replacements)) { + const placeholder = `{{${key}}}`; + modifiedYaml = modifiedYaml.split(placeholder).join(value); + } + return modifiedYaml; +}; diff --git a/frontend/src/__tests__/cypress/test-variables.yml.example b/frontend/src/__tests__/cypress/test-variables.yml.example index dfa626aeee..0cad9a7ed0 100644 --- a/frontend/src/__tests__/cypress/test-variables.yml.example +++ b/frontend/src/__tests__/cypress/test-variables.yml.example @@ -6,4 +6,11 @@ TEST_USER: OCP_ADMIN_USER: AUTH_TYPE: adm-auth USERNAME: adminuser - PASSWORD: adminuser-passwd \ No newline at end of file + PASSWORD: adminuser-passwd +S3: + AWS_ACCESS_KEY_ID: access_key + AWS_SECRET_ACCESS_KEY: secret_key + BUCKET_2: + NAME: pipeline-bucket-name + REGION: pipeline-bucket-region + ENDPOINT: https://pipeline-bucket-endpoint.com/ \ No newline at end of file diff --git a/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline.py b/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline.py new file mode 100644 index 0000000000..f827de8259 --- /dev/null +++ b/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline.py @@ -0,0 +1,19 @@ +from kfp import compiler, dsl + +common_base_image = "registry.redhat.io/ubi8/python-39@sha256:3523b184212e1f2243e76d8094ab52b01ea3015471471290d011625e1763af61" + + +@dsl.component(base_image=common_base_image) +def dummy(message: str): + """Print a message""" + print(message) + + +@dsl.pipeline(name="dummy-pipeline", description="Dummy Pipeline") +def dummy_pipeline(): + dummy_task = dummy(message="Im a dummy pipeline") + + +if __name__ == "__main__": + compiler.Compiler().compile(dummy_pipeline, + package_path=__file__.replace(".py", "_compiled.yaml")) diff --git a/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline_compiled.yaml b/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline_compiled.yaml new file mode 100644 index 0000000000..3da3a75553 --- /dev/null +++ b/frontend/src/__tests__/resources/pipelines_samples/dummy_pipeline_compiled.yaml @@ -0,0 +1,61 @@ +# PIPELINE DEFINITION +# Name: dummy-pipeline +# Description: Dummy Pipeline +components: + comp-dummy: + executorLabel: exec-dummy + inputDefinitions: + parameters: + message: + parameterType: STRING +deploymentSpec: + executors: + exec-dummy: + container: + args: + - --executor_input + - '{{$}}' + - --function_to_execute + - dummy + command: + - sh + - -c + - "\nif ! [ -x \"$(command -v pip)\" ]; then\n python3 -m ensurepip ||\ + \ python3 -m ensurepip --user || apt-get install python3-pip\nfi\n\nPIP_DISABLE_PIP_VERSION_CHECK=1\ + \ python3 -m pip install --quiet --no-warn-script-location 'kfp==2.7.0'\ + \ '--no-deps' 'typing-extensions>=3.7.4,<5; python_version<\"3.9\"' && \"\ + $0\" \"$@\"\n" + - sh + - -ec + - 'program_path=$(mktemp -d) + + + printf "%s" "$0" > "$program_path/ephemeral_component.py" + + _KFP_RUNTIME=true python3 -m kfp.dsl.executor_main --component_module_path "$program_path/ephemeral_component.py" "$@" + + ' + - "\nimport kfp\nfrom kfp import dsl\nfrom kfp.dsl import *\nfrom typing import\ + \ *\n\ndef dummy(message: str):\n \"\"\"Print a message\"\"\"\n print(message)\n\ + \n" + image: registry.redhat.io/ubi8/python-39@sha256:3523b184212e1f2243e76d8094ab52b01ea3015471471290d011625e1763af61 +pipelineInfo: + description: Dummy Pipeline + name: dummy-pipeline +root: + dag: + tasks: + dummy: + cachingOptions: + enableCache: true + componentRef: + name: comp-dummy + inputs: + parameters: + message: + runtimeValue: + constant: Im a dummy pipeline + taskInfo: + name: dummy +schemaVersion: 2.1.0 +sdkVersion: kfp-2.7.0 diff --git a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx index 81358d8474..de9cb53c51 100644 --- a/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx +++ b/frontend/src/concepts/pipelines/content/pipelinesDetails/PipelineDetailsTitle.tsx @@ -33,7 +33,7 @@ const PipelineDetailsTitle: React.FC = ({ )} {statusIcon && ( -