diff --git a/.github/workflows/check-plugins.yaml b/.github/workflows/check-plugins.yaml index 75fd2a2a923..112680f5a2f 100644 --- a/.github/workflows/check-plugins.yaml +++ b/.github/workflows/check-plugins.yaml @@ -2,7 +2,7 @@ name: check-plugins-build on: pull_request: branches: - - master + - master-bananas env: TEST_PERSIST_BUILD: true jobs: diff --git a/.github/workflows/docusaurus.yaml b/.github/workflows/docusaurus.yaml index 8204dc40fb5..b434d892797 100644 --- a/.github/workflows/docusaurus.yaml +++ b/.github/workflows/docusaurus.yaml @@ -4,9 +4,9 @@ on: push: branches: - master - pull_request: - branches: - - master + # pull_request: + # branches: + # - master jobs: build: name: Build diff --git a/.github/workflows/extensions-compatibility-tests.yaml b/.github/workflows/extensions-compatibility-tests.yaml new file mode 100644 index 00000000000..3160b49b519 --- /dev/null +++ b/.github/workflows/extensions-compatibility-tests.yaml @@ -0,0 +1,74 @@ +name: Extensions Compatibility Tests +on: + schedule: + - cron: "0 0 * * *" # runs at midnight every day + # TODO: DELETE THIS!!!! JUST FOR TESTING PURPOSES! + pull_request: + branches: + - master + +env: + TEST_USERNAME: admin + TEST_PASSWORD: password + CATTLE_BOOTSTRAP_PASSWORD: password + TEST_BASE_URL: https://127.0.0.1:8005 + API: https://127.0.0.1 + TEST_PROJECT_ID: rancher-dashboard + CYPRESS_API_URL: http://139.59.134.103:1234/ + TEST_RUN_ID: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests + # Build the dashboard to use in tests. When set to false it will grab `latest` from CDN (useful for running e2e tests quickly) + BUILD_DASHBOARD: true + +jobs: + e2e-test-extensions-compatibility: + strategy: + fail-fast: false + matrix: + role: [ + { username: 'admin', tag: '@adminUser' } + ] + features: [ + ['@elemental', 'elemental'], + ['@kubewarden', 'kubewarden'] + ] + rancherEnv: [ + # ['2.10', 'v2.9-head'], + ['2.9', 'v2.9-head'] + ] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 1 + - name: Setup env + uses: ./.github/actions/setup + + # this is where docker is set up with the enviroment + - name: Run Rancher system ${{ matrix.rancherEnv[0] }} - image:${{ matrix.rancherEnv[1] }} + run: export RANCHER_VERSION_E2E=${{ matrix.rancherEnv[1] }} && yarn e2e:docker + + ## this is just setting up rancher and user + - name: Setup Rancher and user + run: | + yarn e2e:prod + env: + GREP_TAGS: ${{ matrix.role.tag }}Setup+${{ matrix.features[0] }} --@jenkins ${{ matrix.role.tag }}Setup+${{ matrix.features[0] }} --@jenkins + TEST_USERNAME: ${{ matrix.role.username }} + TEST_ONLY: setup + + # This is the actual triggering of the e2e test specs + - name: Run user tests + run: | + export SPEC_FILE="cypress/e2e/tests/pages/extensions-compatibility-tests/${{ matrix.features[1] }}.spec.ts" && yarn e2e:prod + [ "$BUILD_DASHBOARD" != "false" ] || exit 0 + env: + TEST_SKIP: setup + GREP_TAGS: ${{ matrix.role.tag }}+${{ matrix.features[0] }} --@jenkins ${{ matrix.role.tag }}+${{ matrix.features[0] }} --@jenkins + TEST_USERNAME: ${{ matrix.role.username }} + + - name: Upload screenshots + uses: actions/upload-artifact@v3 + if: ${{ failure() }} + with: + name: ${{github.run_number}}-${{github.run_attempt}}-extensions-compatibility-tests-screenshots-${{ matrix.role.tag }}+${{ matrix.features[0] }} + path: cypress/screenshots \ No newline at end of file diff --git a/.github/workflows/storybook.yaml b/.github/workflows/storybook.yaml index b71bc7a58c8..bba2101021b 100644 --- a/.github/workflows/storybook.yaml +++ b/.github/workflows/storybook.yaml @@ -5,9 +5,9 @@ on: push: branches: - master - pull_request: - branches: - - master + # pull_request: + # branches: + # - master jobs: storybook: runs-on: ubuntu-latest diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 80ebb206a44..e93ae062ad5 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -4,10 +4,10 @@ on: branches: - master - 'release-*' - pull_request: - branches: - - master - - 'release-*' + # pull_request: + # branches: + # - master + # - 'release-*' workflow_dispatch: inputs: environment: diff --git a/cypress/e2e/po/components/kubectl.po.ts b/cypress/e2e/po/components/kubectl.po.ts index 841665ca124..d1a99cafecb 100644 --- a/cypress/e2e/po/components/kubectl.po.ts +++ b/cypress/e2e/po/components/kubectl.po.ts @@ -29,8 +29,10 @@ export default class Kubectl extends ComponentPo { * @param command Kube command without the 'kubectl' * @returns executeCommand for method chanining */ - executeCommand(command: string, wait = 3000) { - this.self().get(this.terminalRow).type(`${ this.kubeCommand } ${ command }{enter}`); + executeCommand(command: string, prependKubectl = true, wait = 3000) { + const parsedCommand = prependKubectl ? `${ this.kubeCommand } ${ command }{enter}` : `${ command }{enter}`; + + this.self().get(this.terminalRow).type(parsedCommand); cy.wait(wait); return this; diff --git a/cypress/e2e/po/components/labeled-select.po.ts b/cypress/e2e/po/components/labeled-select.po.ts index ea49ed1eaed..57e4c70e76c 100644 --- a/cypress/e2e/po/components/labeled-select.po.ts +++ b/cypress/e2e/po/components/labeled-select.po.ts @@ -5,6 +5,12 @@ export default class LabeledSelectPo extends ComponentPo { return this.self().click(); } + setOptionAndClick(label: string) { + this.self().get('input[type="search"]').type(label); + + return this.clickOption(1); + } + clickOption(optionIndex: number) { return this.self().get(`.vs__dropdown-menu .vs__dropdown-option:nth-child(${ optionIndex })`).click(); } diff --git a/cypress/e2e/po/pages/extensions-compatibility-tests/elemental.po.ts b/cypress/e2e/po/pages/extensions-compatibility-tests/elemental.po.ts new file mode 100644 index 00000000000..f96c249fd88 --- /dev/null +++ b/cypress/e2e/po/pages/extensions-compatibility-tests/elemental.po.ts @@ -0,0 +1,38 @@ +import ExtensionsCompatibiliyPo from '@/cypress/e2e/po/pages/extensions-compatibility-tests/extensions-compatibility.po'; +import BannersPo from '@/cypress/e2e/po/components/banners.po'; +import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po'; +import RadioGroupInputPo from '@/cypress/e2e/po/components/radio-group-input.po'; +export default class ElementalPo extends ExtensionsCompatibiliyPo { + static url = '/elemental/c/_/dashboard'; + static goTo(): Cypress.Chainable { + return super.goTo(ElementalPo.url); + } + + constructor() { + super(ElementalPo.url); + } + + installOperatorBtnClick(): Cypress.Chainable { + return this.self().getId('charts-install-button').click(); + } + + dashboardCreateElementalClusterClick() { + return this.self().getId('button-create-elemental-cluster').click(); + } + + dashboardCreateUpdateGroupClick() { + return this.self().getId('create-update-group-btn').click(); + } + + elementalClusterSelectorTemplateBanner() { + return new BannersPo('[provider="machineinventoryselectortemplate"] .banner.warning'); + } + + updateGroupTargetClustersSelect() { + return new LabeledSelectPo('[data-testid="cluster-target"]'); + } + + updateGroupImageOption() { + return new RadioGroupInputPo('[data-testid="upgrade-choice-selector"]'); + } +} diff --git a/cypress/e2e/po/pages/extensions-compatibility-tests/extensions-compatibility.po.ts b/cypress/e2e/po/pages/extensions-compatibility-tests/extensions-compatibility.po.ts new file mode 100644 index 00000000000..a2e05e9b890 --- /dev/null +++ b/cypress/e2e/po/pages/extensions-compatibility-tests/extensions-compatibility.po.ts @@ -0,0 +1,145 @@ +import PagePo from '@/cypress/e2e/po/pages/page.po'; +import { InstallChartPage } from '@/cypress/e2e/po/pages/explorer/charts/install-charts.po'; +import Kubectl from '@/cypress/e2e/po/components/kubectl.po'; +import ChartInstalledAppsPagePo from '@/cypress/e2e/po/pages/chart-installed-apps.po'; +import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; +import BaseResourceList from '@/cypress/e2e/po/lists/base-resource-list.po'; +import CodeMirrorPo from '@/cypress/e2e/po/components/code-mirror.po'; +import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po'; +import LabeledInputPo from '@/cypress/e2e/po/components/labeled-input.po'; +import CheckboxInputPo from '@/cypress/e2e/po/components/checkbox-input.po'; +import TabbedPo from '@/cypress/e2e/po/components/tabbed.po'; +import ResourceTablePo from '@/cypress/e2e/po/components/resource-table.po'; +import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po'; + +const installChart = new InstallChartPage(); +const terminal = new Kubectl(); +const installedApps = new ChartInstalledAppsPagePo(); + +export default class ExtensionsCompatibiliyPo extends PagePo { + title(selector: string): Cypress.Chainable { + return this.self().find(selector).invoke('text'); + } + + waitForTitle(selector: string, title: string) { + return this.title(selector).should('contain', title); + } + + goToInstallChartPage(clusterId:string, queryParams = '') { + return installChart.goTo(clusterId, queryParams); + } + + waitForInstallChartPage(repoName:string, chartName:string) { + return installChart.waitForChartPage(repoName, chartName); + } + + chartInstallNext() { + return installChart.nextPage(); + } + + chartInstallClick() { + return installChart.installChart(); + } + + chartInstallWaitForInstallationAndCloseTerminal(interceptName: string, installableParts: Array, beforeTimeout = 15000, requestTimeout = 20000) { + cy.wait(`@${ interceptName }`, { requestTimeout }).its('response.statusCode').should('eq', 201); + + // giving it a small buffer so that the install is properly triggered + cy.wait(beforeTimeout); // eslint-disable-line cypress/no-unnecessary-waiting + terminal.closeTerminal(); + + installableParts.forEach((item:string) => { + installedApps.list().state(item).should('contain', 'Deployed'); + }); + + // timeout to give time for everything to be setup, otherwise the extension + // won't find the chart and show the correct screen + return cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + } + + chartInstallWaitForUpgradeAndCloseTerminal(interceptName: string, beforeTimeout = 15000) { + cy.wait(`@${ interceptName }`, { requestTimeout: 20000 }).its('response.statusCode').should('eq', 201); + + // giving it a small buffer so that the install is properly triggered + cy.wait(beforeTimeout); // eslint-disable-line cypress/no-unnecessary-waiting + terminal.closeTerminal(); + + cy.get('.masthead-state.badge-state').invoke('text').should('contain', 'Deployed'); + + // timeout to give time for everything to be setup, otherwise the extension + // won't find the chart and show the correct screen + return cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + } + + genericWaitForAppToInstall(appName: string, isTerminalOp = true) { + if (isTerminalOp) { + cy.wait(20000); // eslint-disable-line cypress/no-unnecessary-waiting + terminal.closeTerminal(); + } + installedApps.list().state(appName).should('contain', 'Deployed'); + + return cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + } + + sideMenuNavTo(label: string) { + const sideNav = new ProductNavPo(); + + sideNav.navToSideMenuEntryByExactLabel(label); + } + + genericListView(): BaseResourceList { + return new BaseResourceList(this.self()); + } + + goToDetailsPage(elemName: string) { + const resourceTable = new ResourceTablePo(this.self()); + + return resourceTable.sortableTable().detailsPageLinkWithName(elemName).click(); + } + + createFromYamlClick(): Cypress.Chainable { + return this.genericListView().masthead().createYaml(); + } + + createClick(): Cypress.Chainable { + return this.genericListView().masthead().create(); + } + + genericNameInput() { + return LabeledInputPo.bySelector(this.self(), '[data-testid="name-ns-description-name"]'); + } + + genericNamespaceInput(): LabeledSelectPo { + return new LabeledSelectPo('[data-testid="name-ns-description-namespace"]'); + } + + genericLabeledInputByLabel(label:string): LabeledInputPo { + return LabeledInputPo.byLabel(this.self(), label); + } + + genericYamlEditor(): CodeMirrorPo { + return CodeMirrorPo.bySelector(this.self(), '[data-testid="yaml-editor-code-mirror"]'); + } + + saveEditYamlForm(): AsyncButtonPo { + return new AsyncButtonPo('[data-testid="action-button-async-button"]', this.self()); + } + + saveOrCreateResource(): AsyncButtonPo { + return new AsyncButtonPo('[data-testid="form-save"]', this.self()); + } + + saveOrCreateCluster(): AsyncButtonPo { + return new AsyncButtonPo('[data-testid="rke2-custom-create-save"]', this.self()); + } + + genericCheckboxByLabel(label:string): CheckboxInputPo { + return CheckboxInputPo.byLabel(this.self(), label); + } + + clickGenericTab(selector: string) { + const tab = new TabbedPo(); + + return tab.clickTabWithSelector(selector); + } +} diff --git a/cypress/e2e/po/pages/extensions-compatibility-tests/kubewarden.po.ts b/cypress/e2e/po/pages/extensions-compatibility-tests/kubewarden.po.ts new file mode 100644 index 00000000000..935d0369145 --- /dev/null +++ b/cypress/e2e/po/pages/extensions-compatibility-tests/kubewarden.po.ts @@ -0,0 +1,97 @@ +import ExtensionsCompatibiliyPo from '@/cypress/e2e/po/pages/extensions-compatibility-tests/extensions-compatibility.po'; +import Kubectl from '~/cypress/e2e/po/components/kubectl.po'; +import LabeledSelectPo from '@/cypress/e2e/po/components/labeled-select.po'; +import SortableTablePo from '@/cypress/e2e/po/components/sortable-table.po'; +import AsyncButtonPo from '@/cypress/e2e/po/components/async-button.po'; +export default class KubewardenPo extends ExtensionsCompatibiliyPo { + static url = '/c/local/kubewarden'; + static goTo(): Cypress.Chainable { + return super.goTo(KubewardenPo.url); + } + + constructor() { + super(KubewardenPo.url); + } + + startBackendInstallClick(): Cypress.Chainable { + return this.self().getId('kw-initial-install-button').click(); + } + + openTerminalClick() { + return this.self().getId('kw-cm-open-shell').click(); + } + + kubectlShell() { + return new Kubectl(); + } + + waitForCertManagerToInstall() { + return cy.get('[data-testid="kw-repo-add-button"]', { timeout: 30000 }).invoke('text').should('contain', 'Add Kubewarden Repository'); + } + + waitForKwRepoToBeAdded() { + return cy.get('[data-testid="kw-app-install-button"]', { timeout: 30000 }).invoke('text').should('contain', 'Install Kubewarden'); + } + + addKwRepoClick() { + return this.self().getId('kw-repo-add-button').click(); + } + + installOperatorBtnClick(): Cypress.Chainable { + return this.self().getId('kw-app-install-button').click(); + } + + defaultPolicyServerInstallClick() { + return this.self().getId('kw-defaults-banner-button').click(); + } + + rancherMonitoringInstallIntoProjectSelect(optionIndex: number): Cypress.Chainable { + const selectProject = new LabeledSelectPo('.labeled-select.edit'); + + selectProject.toggle(); + + return selectProject.clickOption(optionIndex); + } + + metricsAddServiceMonitorClick() { + this.self().getId('kw-monitoring-checklist-step-service-monitor-button').click(); + } + + metricsAddGrafanaDasboardClick() { + this.self().getId('kw-monitoring-checklist-step-config-map-button').click(); + } + + waitForNamespaceCreation(interceptName: string, namespaceToCheck: string) { + cy.wait(`@${ interceptName }`, { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', namespaceToCheck); + cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting + }); + } + + addToArtifactHubClick() { + this.self().getId('action-button-async-button').click(); + } + + apOfficialPoliciesTable() { + return new SortableTablePo(this.self().get('.sortable-table')); + } + + apOfficialPoliciesTableRowClick(policyName: string) { + this.apOfficialPoliciesTable().rowElementWithName(policyName).scrollIntoView().click(); + } + + apCreateBtn(): AsyncButtonPo { + return new AsyncButtonPo('[data-testid="kw-policy-create-finish-button"]', this.self()); + } + + waitForApCreation(interceptName: string, name: string) { + cy.wait(`@${ interceptName }`, { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body).to.have.property('id', `default/${ name }`); + expect(response?.body.spec).to.have.property('mode', 'protect'); + expect(response?.body.spec.settings).to.have.property('requireTLS', true); + cy.wait(3000); // eslint-disable-line cypress/no-unnecessary-waiting + }); + } +} diff --git a/cypress/e2e/po/pages/extensions.po.ts b/cypress/e2e/po/pages/extensions.po.ts index 06055ef4fc8..97d05c1f2e8 100644 --- a/cypress/e2e/po/pages/extensions.po.ts +++ b/cypress/e2e/po/pages/extensions.po.ts @@ -6,6 +6,7 @@ import NameNsDescriptionPo from '@/cypress/e2e/po/components/name-ns-description import RepositoriesPagePo from '@/cypress/e2e/po/pages/chart-repositories.po'; import BannersPo from '@/cypress/e2e/po/components/banners.po'; import ChartRepositoriesCreateEditPo from '@/cypress/e2e/po/edit/chart-repositories.po'; +import AppClusterRepoEditPo from '@/cypress/e2e/po/edit/catalog.cattle.io.clusterrepo.po'; import { LONG_TIMEOUT_OPT } from '@/cypress/support/utils/timeouts'; export default class ExtensionsPagePo extends PagePo { @@ -83,6 +84,35 @@ export default class ExtensionsPagePo extends PagePo { appRepoList.list().state(name).should('contain', 'Active'); } + /** + * Adds a cluster repo for extensions + * @param repo - The repository url (e.g. https://github.com/rancher/ui-plugin-examples) + * @param branch - The git branch to target + * @param name - A name for the repository + * @returns {Cypress.Chainable} + */ + addExtensionsRepositoryDirectLink(repo: string, branch: string, name: string, waitForActiveState = true): Cypress.Chainable { + const appRepoList = new RepositoriesPagePo('local', 'apps'); + const appRepoCreate = new AppClusterRepoEditPo('local', 'create'); + + appRepoCreate.goTo(); + appRepoCreate.waitForPage(); + + appRepoCreate.nameNsDescription().name().self().scrollIntoView() + .should('be.visible'); + appRepoCreate.nameNsDescription().name().set(name); + appRepoCreate.selectRadioOptionGitRepo(1); + // fill the git repo form + appRepoCreate.enterGitRepoName(repo); + appRepoCreate.enterGitBranchName(branch); + appRepoCreate.create().click(); + + if (waitForActiveState) { + appRepoList.waitForPage(); + appRepoList.list().state(name).should('contain', 'Active'); + } + } + // ------------------ extension card ------------------ extensionCard(extensionName: string) { return this.self().getId(`extension-card-${ extensionName }`); @@ -113,6 +143,14 @@ export default class ExtensionsPagePo extends PagePo { return this.self().get('[data-modal="installPluginDialog"]'); } + installModalSelectVersionLabel(label: string): Cypress.Chainable { + const selectVersion = new LabeledSelectPo(this.extensionInstallModal().getId('install-ext-modal-select-version')); + + selectVersion.toggle(); + + return selectVersion.setOptionAndClick(label); + } + installModalSelectVersionClick(optionIndex: number): Cypress.Chainable { const selectVersion = new LabeledSelectPo(this.extensionInstallModal().getId('install-ext-modal-select-version')); diff --git a/cypress/e2e/po/side-bars/product-side-nav.po.ts b/cypress/e2e/po/side-bars/product-side-nav.po.ts index e8385610a20..365ac5bd0b6 100644 --- a/cypress/e2e/po/side-bars/product-side-nav.po.ts +++ b/cypress/e2e/po/side-bars/product-side-nav.po.ts @@ -45,6 +45,16 @@ export default class ProductNavPo extends ComponentPo { .click({ force: true }); } + /** + * Navigate to a side menu entry by label (exact match) + */ + navToSideMenuEntryByExactLabel(label: string): Cypress.Chainable { + const regexp = new RegExp(`^${ label }$`); + + return this.self().should('exist').find('.child.nav-type a .label').contains(regexp) + .click({ force: true }); + } + /** * Check existence of menu group by label */ diff --git a/cypress/e2e/tests/pages/extensions-compatibility-tests/elemental.spec.ts b/cypress/e2e/tests/pages/extensions-compatibility-tests/elemental.spec.ts new file mode 100644 index 00000000000..2afc61d06db --- /dev/null +++ b/cypress/e2e/tests/pages/extensions-compatibility-tests/elemental.spec.ts @@ -0,0 +1,214 @@ +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; +import ElementalPo from '@/cypress/e2e/po/pages/extensions-compatibility-tests/elemental.po'; +import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; +import * as jsyaml from 'js-yaml'; + +const EXTENSION_NAME = 'elemental'; +const EXTENSION_VERSION = '2.0.0-rc2'; +const EXTENSION_REPO = 'https://github.com/rancher/elemental-ui'; +const EXTENSION_BRANCH = 'gh-pages'; +const EXTENSION_CLUSTER_REPO_NAME = 'elemental-ui-extension'; +const EXTENSION_CHART_CREATION = 'chartCreation'; + +const REG_ENDPOINT_NAME = 'reg-endpoint-1'; +const REG_ENDPOINT_DEVICE_PATH = '/dev/nvme0n123'; + +const MACHINE_INV_NAME = 'machine-inventory-1'; + +const ELEMENTAL_CLUSTER_NAME = 'elemental-cluster-1'; +const ELEMENTAL_CLUSTER_BANNER_TEXT = 'Matches all 1 existing Inventory of Machines'; +const ELEMENTAL_CLUSTER_MACHINE_CONFIG_REF = 'MachineInventorySelectorTemplate'; + +const UPDATE_GROUP_NAME = 'update-group-1'; +const UPDATE_GROUP_IMAGE_PATH = 'some/path'; + +const elementalPo = new ElementalPo(); +const namespacePicker = new NamespaceFilterPo(); + +describe('Extensions Compatibility spec', { tags: ['@elemental', '@adminUser'] }, () => { + beforeEach(() => { + cy.login(); + }); + + it('add extension repository', () => { + // // This should be in a `before` however is flaky. Move it to an `it` to let cypress retry + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.addExtensionsRepositoryDirectLink(EXTENSION_REPO, EXTENSION_BRANCH, EXTENSION_CLUSTER_REPO_NAME, true); + }); + + it('Should install an extension', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.goTo(); + + extensionsPo.extensionTabAvailableClick(); + + // click on install button on card + extensionsPo.extensionCardInstallClick(EXTENSION_NAME); + extensionsPo.extensionInstallModal().should('be.visible'); + + // select version and click install + extensionsPo.installModalSelectVersionLabel(EXTENSION_VERSION); + extensionsPo.installModalInstallClick(); + + // let's check the extension reload banner and reload the page + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + // make sure extension card is in the installed tab + extensionsPo.extensionTabInstalledClick(); + extensionsPo.extensionCardClick(EXTENSION_NAME); + extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + extensionsPo.extensionDetailsCloseClick(); + }); + + it('Should setup all of the needed backend parts', () => { + cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/rancher-charts?action=install').as(EXTENSION_CHART_CREATION); + + elementalPo.goTo(); + elementalPo.waitForTitle('h1', 'OS Management'); + + elementalPo.installOperatorBtnClick(); + + elementalPo.waitForInstallChartPage('rancher-charts', 'elemental'); + + // we need to change the namespace picker in order for the install check on the list view + namespacePicker.toggle(); + namespacePicker.clickOptionByLabel('All Namespaces'); + namespacePicker.closeDropdown(); + + elementalPo.chartInstallNext(); + elementalPo.chartInstallClick(); + elementalPo.chartInstallWaitForInstallationAndCloseTerminal(EXTENSION_CHART_CREATION, ['elemental-operator-crds', 'elemental-operator']); + + elementalPo.goTo(); + elementalPo.waitForTitle('[data-testid="elemental-main-title"]', 'OS Management Dashboard'); + }); + + it('Should create an Elemental registration endpoint', () => { + cy.intercept('POST', 'v1/elemental.cattle.io.machineregistrations/fleet-default').as('machineRegCreation'); + + elementalPo.goTo(); + elementalPo.sideMenuNavTo('Registration Endpoints'); + elementalPo.createClick(); + + elementalPo.genericNameInput().set(REG_ENDPOINT_NAME); + elementalPo.genericYamlEditor().value().then((val) => { + // convert yaml into json to update values + const json: any = jsyaml.load(val); + + json.config.elemental.install.device = REG_ENDPOINT_DEVICE_PATH; + + elementalPo.genericYamlEditor().set(jsyaml.dump(json)); + elementalPo.saveOrCreateResource().click(); + + cy.wait('@machineRegCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', REG_ENDPOINT_NAME); + expect(response?.body.spec.config.elemental.install).to.have.property('device', REG_ENDPOINT_DEVICE_PATH); + }); + }); + }); + + it('Should create an Elemental resource via YAML (Inventory of Machines)', () => { + function poolingSchemaDefinition() { + cy + .request({ + url: 'v1/schemaDefinitions/elemental.cattle.io.machineinventory', + method: 'GET', + failOnStatusCode: false + }) + .then((resp) => { + if (resp.status === 200) { + return; + } + + // let's wait for a bit so that we don't overload the server + // with requests + cy.wait(5000); // eslint-disable-line cypress/no-unnecessary-waiting + poolingSchemaDefinition(); + }); + } + + elementalPo.goTo(); + elementalPo.sideMenuNavTo('Inventory of Machines'); + // after we hit create from YAML we need to pool for the schemaDefinition since + // that takes while to be available https://docs.cypress.io/api/commands/request#Request-Polling + elementalPo.createFromYamlClick().then(poolingSchemaDefinition); + + elementalPo.genericYamlEditor().value().then((val) => { + // convert yaml into json to update values + const json: any = jsyaml.load(val); + + json.metadata.name = MACHINE_INV_NAME; + + elementalPo.genericYamlEditor().set(jsyaml.dump(json)); + elementalPo.saveEditYamlForm().click(); + + elementalPo.waitForPageWithSpecificUrl('/elemental/c/_/elemental.cattle.io.machineinventory'); + elementalPo.genericListView().rowWithName(MACHINE_INV_NAME).column(2).should('contain', MACHINE_INV_NAME); + }); + }); + + it('Should create an Elemental cluster, targeting all of the inventory of machines', () => { + cy.intercept('POST', 'v1/provisioning.cattle.io.clusters').as('elementalClusterCreation'); + + elementalPo.goTo(); + elementalPo.dashboardCreateElementalClusterClick(); + + elementalPo.genericNameInput().set(ELEMENTAL_CLUSTER_NAME); + elementalPo.elementalClusterSelectorTemplateBanner().banner().contains(ELEMENTAL_CLUSTER_BANNER_TEXT); + elementalPo.saveOrCreateCluster().click(); + + cy.wait('@elementalClusterCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', ELEMENTAL_CLUSTER_NAME); + expect(response?.body.spec.rkeConfig.machinePools[0].machineConfigRef).to.have.property('kind', ELEMENTAL_CLUSTER_MACHINE_CONFIG_REF); + }); + }); + + it('Should create an Upgrade Group', () => { + cy.intercept('POST', 'v1/elemental.cattle.io.managedosimages').as('elementalUpdateGroupCreation'); + + elementalPo.goTo(); + elementalPo.dashboardCreateUpdateGroupClick(); + + elementalPo.genericNameInput().set(UPDATE_GROUP_NAME); + elementalPo.updateGroupTargetClustersSelect().toggle(); + elementalPo.updateGroupTargetClustersSelect().clickOptionWithLabel(ELEMENTAL_CLUSTER_NAME); + elementalPo.updateGroupImageOption().set(1); + + elementalPo.genericLabeledInputByLabel('Image path').set(UPDATE_GROUP_IMAGE_PATH); + elementalPo.saveOrCreateResource().click(); + + cy.wait('@elementalUpdateGroupCreation', { requestTimeout: 15000 }).then(({ response }) => { + expect(response?.statusCode).to.eq(201); + expect(response?.body.metadata).to.have.property('name', UPDATE_GROUP_NAME); + expect(response?.body.spec.clusterTargets[0]).to.have.property('clusterName', ELEMENTAL_CLUSTER_NAME); + expect(response?.body.spec).to.have.property('osImage', UPDATE_GROUP_IMAGE_PATH); + }); + }); + + it('Should uninstall the extension', () => { + const extensionsPo = new ExtensionsPagePo(); + + extensionsPo.goTo(); + extensionsPo.extensionTabInstalledClick(); + + // click on uninstall button on card + extensionsPo.extensionCardUninstallClick(EXTENSION_NAME); + extensionsPo.extensionUninstallModal().should('be.visible'); + extensionsPo.uninstallModaluninstallClick(); + extensionsPo.extensionReloadBanner().should('be.visible'); + + // let's check the extension reload banner and reload the page + extensionsPo.extensionReloadBanner().should('be.visible'); + extensionsPo.extensionReloadClick(); + + // make sure extension card is in the available tab + extensionsPo.extensionTabAvailableClick(); + extensionsPo.extensionCardClick(EXTENSION_NAME); + extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + }); +}); diff --git a/cypress/e2e/tests/pages/extensions-compatibility-tests/kubewarden.spec.ts b/cypress/e2e/tests/pages/extensions-compatibility-tests/kubewarden.spec.ts new file mode 100644 index 00000000000..5a2a5328757 --- /dev/null +++ b/cypress/e2e/tests/pages/extensions-compatibility-tests/kubewarden.spec.ts @@ -0,0 +1,296 @@ +import ExtensionsPagePo from '@/cypress/e2e/po/pages/extensions.po'; +import KubewardenPo from '@/cypress/e2e/po/pages/extensions-compatibility-tests/kubewarden.po'; +import { NamespaceFilterPo } from '@/cypress/e2e/po/components/namespace-filter.po'; +import ChartInstalledAppsPagePo from '@/cypress/e2e/po/pages/chart-installed-apps.po'; +import ProjectsNamespacesPagePo from '@/cypress/e2e/po/pages/explorer/projects-namespaces.po'; +import ProductNavPo from '@/cypress/e2e/po/side-bars/product-side-nav.po'; +import { IngressPagePo } from '@/cypress/e2e/po/pages/explorer/ingress.po'; +// import * as jsyaml from 'js-yaml'; + +const EXTENSION_NAME = 'kubewarden'; +const EXTENSION_VERSION = '2.0.0'; +const EXTENSION_REPO = 'https://github.com/rancher/kubewarden-ui'; +const EXTENSION_BRANCH = 'gh-pages'; +const EXTENSION_CLUSTER_REPO_NAME = 'kubewarden-ui-extension'; +// const EXTENSION_KW_REPO_ADD = 'addKwRepo'; +const RANCHER_CHART_CREATION = 'rancherCreation'; +const MAIN_EXTENSION_CHART_CREATION = 'chartCreation'; +const MAIN_EXTENSION_CHART_UPGRADE = 'chartUpgrade'; + +const DUMMY_NAMESPACE = 'another-dummy-namespace'; +const NAMESPACE_CREATION = 'namespaceCreation'; + +const INGRESS_POLICY_NAME = 'some-ingress-policy-name'; +const AP_CREATION = 'apCreation'; + +const kubewardenPo = new KubewardenPo(); +const namespacePicker = new NamespaceFilterPo(); +const installedApps = new ChartInstalledAppsPagePo('local', 'apps'); +const projectsNamespacesPage = new ProjectsNamespacesPagePo('local'); +const sideNav = new ProductNavPo(); +const ingressPagePo = new IngressPagePo(); + +describe('Extensions Compatibility spec', { tags: ['@kubewarden', '@adminUser'] }, () => { + beforeEach(() => { + cy.login(); + }); + + // it('add extension repository', () => { + // // // This should be in a `before` however is flaky. Move it to an `it` to let cypress retry + // const extensionsPo = new ExtensionsPagePo(); + + // extensionsPo.addExtensionsRepositoryDirectLink(EXTENSION_REPO, EXTENSION_BRANCH, EXTENSION_CLUSTER_REPO_NAME, true); + // cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + // }); + + // it('Should install an extension', () => { + // const extensionsPo = new ExtensionsPagePo(); + + // extensionsPo.goTo(); + + // extensionsPo.extensionTabAvailableClick(); + + // // click on install button on card + // extensionsPo.extensionCardInstallClick(EXTENSION_NAME); + // extensionsPo.extensionInstallModal().should('be.visible'); + + // // select version and click install + // extensionsPo.installModalSelectVersionLabel(EXTENSION_VERSION); + // extensionsPo.installModalInstallClick(); + + // // let's check the extension reload banner and reload the page + // extensionsPo.extensionReloadBanner().should('be.visible'); + // extensionsPo.extensionReloadClick(); + + // // make sure extension card is in the installed tab + // extensionsPo.extensionTabInstalledClick(); + // extensionsPo.extensionCardClick(EXTENSION_NAME); + // extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + // extensionsPo.extensionDetailsCloseClick(); + // }); + + // it('Should setup all of the needed backend parts', () => { + // cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/rancher-charts?action=install').as(RANCHER_CHART_CREATION); + // cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/kubewarden-charts?action=install').as(MAIN_EXTENSION_CHART_CREATION); + // cy.intercept('POST', 'v1/catalog.cattle.io.clusterrepos/kubewarden-charts?action=upgrade').as(MAIN_EXTENSION_CHART_UPGRADE); + + // kubewardenPo.goTo(); + // kubewardenPo.waitForTitle('h1', 'Kubewarden'); + + // // we need to change the namespace picker in order for the install check on the list view + // namespacePicker.toggle(); + // namespacePicker.clickOptionByLabel('All Namespaces'); + // namespacePicker.closeDropdown(); + + // // start install steps + // kubewardenPo.startBackendInstallClick(); + + // // 1 - install cert manager + // kubewardenPo.openTerminalClick(); + // kubewardenPo.kubectlShell().executeCommand('kubectl apply -f https://github.com/jetstack/cert-manager/releases/latest/download/cert-manager.yaml', false); + // kubewardenPo.waitForCertManagerToInstall(); + // kubewardenPo.kubectlShell().closeTerminal(); + + // // 2 - Add kubewarden repository + // kubewardenPo.addKwRepoClick(); + // kubewardenPo.waitForKwRepoToBeAdded(); + + // // 3 - Install kubewarden operator + // kubewardenPo.installOperatorBtnClick(); + // kubewardenPo.waitForInstallChartPage('kubewarden-charts', 'kubewarden-controller'); + // kubewardenPo.chartInstallNext(); + // kubewardenPo.genericCheckboxByLabel('Enable Policy Reporter UI').set(); + // kubewardenPo.chartInstallClick(); + // kubewardenPo.chartInstallWaitForInstallationAndCloseTerminal(MAIN_EXTENSION_CHART_CREATION, ['rancher-kubewarden-controller', 'rancher-kubewarden-crds'], 60000); + + // kubewardenPo.goTo(); + // kubewardenPo.waitForTitle('[data-testid="kw-dashboard-title"]', 'Welcome to Kubewarden'); + + // // 4 - add default policy server charts + // kubewardenPo.defaultPolicyServerInstallClick(); + // kubewardenPo.waitForInstallChartPage('kubewarden-charts', 'kubewarden-defaults'); + // kubewardenPo.chartInstallNext(); + // kubewardenPo.genericCheckboxByLabel('Enable recommended policies').set(); + // kubewardenPo.chartInstallClick(); + // kubewardenPo.chartInstallWaitForInstallationAndCloseTerminal(MAIN_EXTENSION_CHART_CREATION, ['rancher-kubewarden-defaults'], 30000); + + // installedApps.goTo(); + // installedApps.waitForPage(); + + // // 5 - Install Tracing parts + // // 5.1 - Open Telemetry Operator as per https://docs.kubewarden.io/next/howtos/telemetry/opentelemetry-qs#install-opentelemetry + // const addOTCommand = `helm repo add open-telemetry https://open-telemetry.github.io/opentelemetry-helm-charts + + // helm install --wait \ + // --namespace open-telemetry \ + // --create-namespace \ + // --version 0.56.0 \ + // --set "manager.collectorImage.repository=otel/opentelemetry-collector-contrib" \ + // my-opentelemetry-operator open-telemetry/opentelemetry-operator`; + + // kubewardenPo.kubectlShell().openTerminal(); + // kubewardenPo.kubectlShell().executeCommand(addOTCommand, false); + // kubewardenPo.genericWaitForAppToInstall('my-opentelemetry-operator'); + + // // 5.2 - Jaeger Operator as per https://docs.kubewarden.io/next/howtos/telemetry/tracing-qs#install-jaeger + // const addJaegerOperatorCommand = `helm repo add jaegertracing https://jaegertracing.github.io/helm-charts + + // helm upgrade -i --wait \ + // --namespace jaeger \ + // --create-namespace \ + // --version 2.49.0 \ + // jaeger-operator jaegertracing/jaeger-operator \ + // --set rbac.clusterRole=true`; + + // kubewardenPo.kubectlShell().openTerminal(); + // kubewardenPo.kubectlShell().executeCommand(addJaegerOperatorCommand, false); + // kubewardenPo.genericWaitForAppToInstall('jaeger-operator'); + + // // 5.3 - Create Jaeger Resource as per https://docs.kubewarden.io/next/howtos/telemetry/tracing-qs#install-jaeger + // const createJaegerResource = `kubectl apply -f - < { + cy.intercept('POST', 'v1/namespaces').as(NAMESPACE_CREATION); + cy.intercept('POST', 'v1/policies.kubewarden.io.admissionpolicies/default').as(AP_CREATION); + // create a new namespace + // create an AP -> keep it protect mode + // prepare a resource to trigger that specific AP + // go to cron job and force run it + // go to a given namespace and check the compliance report to see the result + // projectsNamespacesPage.goTo(); + // projectsNamespacesPage.waitForPage(); + // projectsNamespacesPage.flatListClick(); // easier to trigger a namespace creation + // projectsNamespacesPage.createProjectNamespaceClick(); + // projectsNamespacesPage.name().set(DUMMY_NAMESPACE); + // projectsNamespacesPage.buttonSubmit().click(); + + // kubewardenPo.waitForNamespaceCreation(NAMESPACE_CREATION, DUMMY_NAMESPACE); + + kubewardenPo.goTo(); + kubewardenPo.waitForTitle('[data-testid="kw-dashboard-title"]', 'Welcome to Kubewarden'); + + kubewardenPo.sideMenuNavTo('AdmissionPolicies'); + kubewardenPo.waitForPage(); + + kubewardenPo.createClick(); + // kubewardenPo.addToArtifactHubClick(); + // // let's wait for the whitelisting to do it's thing + // cy.wait(10000); // eslint-disable-line cypress/no-unnecessary-waiting + + kubewardenPo.apOfficialPoliciesTableRowClick('Ingress Policy'); + + kubewardenPo.genericNamespaceInput().toggle(); + kubewardenPo.genericNamespaceInput().clickOptionWithLabel(DUMMY_NAMESPACE); + kubewardenPo.genericNameInput().set(INGRESS_POLICY_NAME); + kubewardenPo.clickGenericTab('[data-testid="btn-Settings"]'); + kubewardenPo.genericCheckboxByLabel('Require TLS').set(); + + // kubewardenPo.apCreateBtn().click(); + // kubewardenPo.waitForApCreation(AP_CREATION, INGRESS_POLICY_NAME); + + // HERE WE'LL NEED TO WAIT FOR THE POLICY TO COME UP! + + // sideNav.navToSideMenuGroupByLabel('Service Discovery'); + // sideNav.navToSideMenuEntryByLabel('Ingresses'); + + // ingressPagePo.waitForPage(); + // ingressPagePo.clickCreate(); + }); + + // it('Should create a Policy Server via YAML', () => { + // // create a working Policy Server via YAML + // }); + + // it('Should uninstall the extension', () => { + // const extensionsPo = new ExtensionsPagePo(); + + // extensionsPo.goTo(); + // extensionsPo.extensionTabInstalledClick(); + + // // click on uninstall button on card + // extensionsPo.extensionCardUninstallClick(EXTENSION_NAME); + // extensionsPo.extensionUninstallModal().should('be.visible'); + // extensionsPo.uninstallModaluninstallClick(); + // extensionsPo.extensionReloadBanner().should('be.visible'); + + // // let's check the extension reload banner and reload the page + // extensionsPo.extensionReloadBanner().should('be.visible'); + // extensionsPo.extensionReloadClick(); + + // // make sure extension card is in the available tab + // extensionsPo.extensionTabAvailableClick(); + // extensionsPo.extensionCardClick(EXTENSION_NAME); + // extensionsPo.extensionDetailsTitle().should('contain', EXTENSION_NAME); + // }); +}); diff --git a/cypress/e2e/tests/setup/rancher-setup.spec.ts b/cypress/e2e/tests/setup/rancher-setup.spec.ts index 067353090a7..314f9d4bea6 100644 --- a/cypress/e2e/tests/setup/rancher-setup.spec.ts +++ b/cypress/e2e/tests/setup/rancher-setup.spec.ts @@ -6,7 +6,7 @@ import { serverUrlLocalhostCases, urlWithTrailingForwardSlash, httpUrl, nonUrlCa // Cypress or the GrepTags avoid to run multiples times the same test for each tag used. // This is a temporary solution till initialization is not handled as a test -describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@vai'] }, () => { +describe('Rancher setup', { tags: ['@adminUserSetup', '@standardUserSetup', '@setup', '@components', '@navigation', '@charts', '@explorer', '@extensions', '@fleet', '@generic', '@globalSettings', '@manager', '@userMenu', '@usersAndAuths', '@elemental', '@kubewarden', '@vai'] }, () => { const rancherSetupLoginPage = new RancherSetupLoginPagePo(); const rancherSetupConfigurePage = new RancherSetupConfigurePage(); const homePage = new HomePagePo(); diff --git a/package.json b/package.json index 24f4c3f0c7d..ccf36857384 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,11 @@ "cy:e2e": "cypress open --e2e --browser chrome", "cy:open": "cypress open", "cy:run": "cypress run --browser chrome", - "cy:run:sorry": "./scripts/e2e", + "cy:run:sorry": "./scripts/e2e $SPEC_FILE", "e2e:pre-dev": "yarn docker:local:stop && yarn docker:local:start && NODE_ENV=dev TEST_INSTRUMENT=true yarn build", "e2e:dev": "START_SERVER_AND_TEST_INSECURE=1 server-test start:dev https-get://localhost:8005 cy:run:sorry", "e2e:build": "mkdir dist && TEST_INSTRUMENT=true ./scripts/build-e2e", - "e2e:docker": "yarn docker:local:stop && ./scripts/e2e-docker-start ", + "e2e:docker": "yarn docker:local:stop && ./scripts/e2e-docker-start $RANCHER_VERSION_E2E", "e2e:prod": "BUILD_DASHBOARD=$BUILD_DASHBOARD GREP_TAGS=$GREP_TAGS TEST_USERNAME=$TEST_USERNAME TEST_BASE_URL=https://127.0.0.1/dashboard yarn cy:run:sorry", "coverage": "npx nyc merge coverage coverage/coverage.json", "storybook": "cd storybook && yarn install && yarn storybook", diff --git a/scripts/e2e b/scripts/e2e index d961830d62e..34029497c7a 100755 --- a/scripts/e2e +++ b/scripts/e2e @@ -2,4 +2,13 @@ ID="$(echo ${TEST_RUN_ID:-`date +%s` } | tr '[:upper:]' '[:lower:]' | tr ' ' '-')" echo "$ID" -CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" + + +# check if script invoke contains any argument. If so, add spec to run +if [ $# -eq 1 ]; then + echo "on the e2e script file where SPEC=$1" + CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run --spec "$1" --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" +else + echo "normal e2e script" + CYPRESS_coverage=true CYPRESS_API_URL='http://139.59.134.103:1234' cy2 run --group "$GREP_TAGS" --browser chrome --record --key rancher-dashboard --parallel --ci-build-id "$ID" +fi diff --git a/scripts/e2e-docker-start b/scripts/e2e-docker-start index 13944eedfe7..21a03a07bef 100755 --- a/scripts/e2e-docker-start +++ b/scripts/e2e-docker-start @@ -8,16 +8,39 @@ EMBER_DIST=${DIR}/dist_ember # Image version RANCHER_IMG_VERSION=v2.9-head +HAS_ARGS="false" -docker run -d --restart=unless-stopped -p 80:80 -p 443:443 \ - -v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard \ - -v ${EMBER_DIST}:/usr/share/rancher/ui \ - -e CATTLE_UI_OFFLINE_PREFERRED=true \ - -e CATTLE_BOOTSTRAP_PASSWORD=password \ - -e CATTLE_PASSWORD_MIN_LENGTH=3 \ - --name cypress \ - --privileged \ - rancher/rancher:${RANCHER_IMG_VERSION} +# check if script invoke contains any argument. If so, adjust RANCHER_IMG_VERSION +if [ $# -eq 1 ]; then + RANCHER_IMG_VERSION=$1 + HAS_ARGS="true" +fi + +if [ "$HAS_ARGS" == "false" ]; then + # this flow is what test.yaml is expected to to be following + echo "normal flow without args. Defaults to v2.9-head" + + docker run -d --restart=unless-stopped -p 80:80 -p 443:443 \ + -v ${DASHBOARD_DIST}:/usr/share/rancher/ui-dashboard/dashboard \ + -v ${EMBER_DIST}:/usr/share/rancher/ui \ + -e CATTLE_UI_OFFLINE_PREFERRED=true \ + -e CATTLE_BOOTSTRAP_PASSWORD=password \ + -e CATTLE_PASSWORD_MIN_LENGTH=3 \ + --name cypress \ + --privileged \ + rancher/rancher:${RANCHER_IMG_VERSION} +else + # We will only hit this scenario for the extensions-compatibility-tests workflow and if it's not a -head version + echo "special flow with version arg... RANCHER_IMG_VERSION=${RANCHER_IMG_VERSION}" + + docker run -d --restart=unless-stopped -p 80:80 -p 443:443 \ + -e CATTLE_UI_OFFLINE_PREFERRED=true \ + -e CATTLE_BOOTSTRAP_PASSWORD=password \ + -e CATTLE_PASSWORD_MIN_LENGTH=3 \ + --name cypress \ + --privileged \ + rancher/rancher:${RANCHER_IMG_VERSION} +fi docker ps