From d385e75189c7142aab3fafd24fd9befca5f1f653 Mon Sep 17 00:00:00 2001
From: Marniks7 <9289172+marniks7@users.noreply.github.com>
Date: Sun, 11 Dec 2022 21:39:17 -0500
Subject: [PATCH] Add new action for PipelineRun - Edit and run
---
.../cypress/e2e/run/actions-pipelinerun.cy.js | 81 ++++++++
src/api/pipelineRuns.js | 52 ++++--
src/api/pipelineRuns.test.js | 176 ++++++++++++++++++
.../CreatePipelineRun/CreatePipelineRun.js | 39 +++-
.../CreatePipelineRun.test.js | 111 ++++++++++-
.../CreatePipelineRun/YAMLEditor.js | 49 +++--
.../CreatePipelineRun/YAMLEditor.test.js | 10 +
src/containers/PipelineRun/PipelineRun.js | 15 ++
src/containers/PipelineRuns/PipelineRuns.js | 15 ++
src/nls/messages_de.json | 2 +
src/nls/messages_en.json | 2 +
src/nls/messages_es.json | 2 +
src/nls/messages_fr.json | 2 +
src/nls/messages_it.json | 2 +
src/nls/messages_ja.json | 2 +
src/nls/messages_ko.json | 2 +
src/nls/messages_pt.json | 2 +
src/nls/messages_zh-Hans.json | 2 +
src/nls/messages_zh-Hant.json | 2 +
19 files changed, 531 insertions(+), 37 deletions(-)
create mode 100644 packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js
diff --git a/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js b/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js
new file mode 100644
index 0000000000..ef0f785a94
--- /dev/null
+++ b/packages/e2e/cypress/e2e/run/actions-pipelinerun.cy.js
@@ -0,0 +1,81 @@
+/*
+Copyright 2022 The Tekton Authors
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+const namespace = 'e2e-actions';
+describe('Edit and run Pipeline Run', () => {
+ before(() => {
+ cy.exec('kubectl version --client');
+ cy.exec(`kubectl create namespace ${namespace} || true`);
+ });
+
+ after(() => {
+ cy.exec(`kubectl delete namespace ${namespace} || true`);
+ });
+
+ it('should create pipelinerun on edit and run', function () {
+ // given pipeline
+ const uniqueNumber = Date.now();
+ const pipelineName = `sp-${uniqueNumber}`;
+ const pipeline = `apiVersion: tekton.dev/v1beta1
+kind: Pipeline
+metadata:
+ name: ${pipelineName}
+ namespace: ${namespace}
+spec:
+ tasks:
+ - name: hello
+ taskSpec:
+ steps:
+ - name: echo
+ image: busybox
+ script: |
+ #!/bin/ash
+ echo "Hello World!"
+ `;
+ cy.exec(`echo "${pipeline}" | kubectl apply -f -`);
+ // when create first pipeline run
+ cy.visit(
+ `/#/pipelineruns/create?namespace=${namespace}&pipelineName=${pipelineName}`
+ );
+ cy.get('[id=create-pipelinerun--namespaces-dropdown]').should(
+ 'have.value',
+ namespace
+ );
+ cy.get('[id=create-pipelinerun--pipelines-dropdown]').should(
+ 'have.value',
+ pipelineName
+ );
+ cy.contains('button', 'Create').click();
+
+ cy.contains('h1', 'PipelineRuns');
+
+ cy.get(
+ `td:has(.bx--link[title*=${pipelineName}-run]) + td:has(.tkn--status[data-reason=Succeeded])`,
+ { timeout: 15000 }
+ ).should('have.length', 1);
+
+ // when edit and run to create second pipeline run
+ cy.contains('a', `${pipelineName}-run`).click();
+ cy.contains('button', 'Actions').click();
+ cy.contains('button', 'Edit and run').click();
+ cy.get('.cm-content').contains(`name: ${pipelineName}`);
+ cy.contains('button', 'Create').click();
+ cy.contains('h1', 'PipelineRuns');
+
+ // then
+ cy.get(
+ `td:has(.bx--link[title*=${pipelineName}-run]) + td:has(.tkn--status[data-reason=Succeeded])`,
+ { timeout: 15000 }
+ ).should('have.length', 2);
+ });
+});
diff --git a/src/api/pipelineRuns.js b/src/api/pipelineRuns.js
index bf7d0db77c..301a13581d 100644
--- a/src/api/pipelineRuns.js
+++ b/src/api/pipelineRuns.js
@@ -170,22 +170,32 @@ export function createPipelineRun({
return post(uri, payload).then(({ body }) => body);
}
-export function rerunPipelineRun(pipelineRun) {
- const { annotations, labels, name, namespace } = pipelineRun.metadata;
+export function generateNewPipelineRunPayload({ pipelineRun, rerun }) {
+ const { annotations, labels, name, namespace, generateName } =
+ pipelineRun.metadata;
const payload = deepClone(pipelineRun);
payload.apiVersion =
payload.apiVersion || `tekton.dev/${getTektonPipelinesAPIVersion()}`;
payload.kind = payload.kind || 'PipelineRun';
+
+ function getGenerateName() {
+ if (rerun) {
+ return getGenerateNamePrefixForRerun(name);
+ }
+
+ return generateName || `${name}-`;
+ }
+
payload.metadata = {
annotations: annotations || {},
- generateName: getGenerateNamePrefixForRerun(name),
- labels: {
- ...labels,
- 'dashboard.tekton.dev/rerunOf': name
- },
+ generateName: getGenerateName(),
+ labels: labels || {},
namespace
};
+ if (rerun) {
+ payload.metadata.labels['dashboard.tekton.dev/rerunOf'] = name;
+ }
Object.keys(payload.metadata.labels).forEach(label => {
if (label.startsWith('tekton.dev/')) {
@@ -194,24 +204,36 @@ export function rerunPipelineRun(pipelineRun) {
});
/*
- This is used by Tekton Pipelines as part of the conversion between v1beta1
- and v1 resources. Creating a run with this in place prevents it from actually
- executing and instead adopts the status of the original TaskRuns.
+ This is used by Tekton Pipelines as part of the conversion between v1beta1
+ and v1 resources. Creating a run with this in place prevents it from actually
+ executing and instead adopts the status of the original TaskRuns.
- Ideally we would just delete all `tekton.dev/*` annotations as we do with labels but
- `tekton.dev/v1beta1Resources` is required for pipelines that use PipelineResources,
- and there may be other similar annotations that are still required.
+ Ideally we would just delete all `tekton.dev/*` annotations as we do with labels but
+ `tekton.dev/v1beta1Resources` is required for pipelines that use PipelineResources,
+ and there may be other similar annotations that are still required.
- When v1beta1 has been fully removed from Tekton Pipelines we can revisit this
- and remove all remaining `tekton.dev/*` annotations.
+ When v1beta1 has been fully removed from Tekton Pipelines we can revisit this
+ and remove all remaining `tekton.dev/*` annotations.
*/
delete payload.metadata.annotations['tekton.dev/v1beta1TaskRuns'];
delete payload.metadata.annotations[
'kubectl.kubernetes.io/last-applied-configuration'
];
+ Object.keys(payload.metadata).forEach(
+ i => payload.metadata[i] === undefined && delete payload.metadata[i]
+ );
delete payload.status;
+
delete payload.spec?.status;
+ return { namespace, payload };
+}
+
+export function rerunPipelineRun(pipelineRun) {
+ const { namespace, payload } = generateNewPipelineRunPayload({
+ pipelineRun,
+ rerun: true
+ });
const uri = getTektonAPI('pipelineruns', { namespace });
return post(uri, payload).then(({ body }) => body);
diff --git a/src/api/pipelineRuns.test.js b/src/api/pipelineRuns.test.js
index 1be357ea6c..163daa839c 100644
--- a/src/api/pipelineRuns.test.js
+++ b/src/api/pipelineRuns.test.js
@@ -11,9 +11,11 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
+import yaml from 'js-yaml';
import * as API from './pipelineRuns';
import * as utils from './utils';
import { rest, server } from '../../config_frontend/msw';
+import { generateNewPipelineRunPayload } from './pipelineRuns';
it('cancelPipelineRun', () => {
const name = 'foo';
@@ -326,3 +328,177 @@ it('startPipelineRun', () => {
}
);
});
+
+it('rerun. generate new pipeline run. minimum', () => {
+ const pipelineRun = {
+ apiVersion: 'tekton.dev/v1beta1',
+ kind: 'PipelineRun',
+ metadata: {
+ name: 'test',
+ namespace: 'test-namespace'
+ },
+ spec: {
+ pipelineRef: {
+ name: 'simple'
+ }
+ }
+ };
+ const { namespace, payload } = generateNewPipelineRunPayload({
+ pipelineRun,
+ rerun: true
+ });
+ const expected = `apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+ annotations: {}
+ generateName: test-r-
+ labels:
+ dashboard.tekton.dev/rerunOf: test
+ namespace: test-namespace
+spec:
+ pipelineRef:
+ name: simple
+`;
+ expect(namespace).toEqual('test-namespace');
+ expect(yaml.dump(payload)).toEqual(expected);
+});
+
+it('rerun. generate new pipeline run. maximum', () => {
+ const pipelineRun = {
+ apiVersion: 'tekton.dev/v1beta1',
+ kind: 'PipelineRun',
+ metadata: {
+ name: 'test',
+ namespace: 'test-namespace',
+ annotations: {
+ keya: 'valuea',
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion": "tekton.dev/v1beta1", "keya": "valuea"}'
+ },
+ labels: {
+ key1: 'valuel',
+ key2: 'value2',
+ 'tekton.dev/pipeline': 'foo'
+ },
+ uid: '111-233-33',
+ resourceVersion: 'aaaa'
+ },
+ spec: {
+ pipelineRef: {
+ name: 'simple'
+ },
+ params: [{ name: 'param-1' }, { name: 'param-2' }]
+ },
+ status: { startTime: '0' }
+ };
+ const { namespace, payload } = generateNewPipelineRunPayload({
+ pipelineRun,
+ rerun: true
+ });
+ const expected = `apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+ annotations:
+ keya: valuea
+ generateName: test-r-
+ labels:
+ key1: valuel
+ key2: value2
+ dashboard.tekton.dev/rerunOf: test
+ namespace: test-namespace
+spec:
+ pipelineRef:
+ name: simple
+ params:
+ - name: param-1
+ - name: param-2
+`;
+ expect(namespace).toEqual('test-namespace');
+ expect(yaml.dump(payload)).toEqual(expected);
+});
+
+it('edit. generate new pipeline run. minimum', () => {
+ const pipelineRun = {
+ apiVersion: 'tekton.dev/v1beta1',
+ kind: 'PipelineRun',
+ metadata: {
+ name: 'test',
+ namespace: 'test-namespace'
+ },
+ spec: {
+ pipelineRef: {
+ name: 'simple'
+ }
+ }
+ };
+ const { namespace, payload } = generateNewPipelineRunPayload({
+ pipelineRun,
+ rerun: false
+ });
+ const expected = `apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+ annotations: {}
+ generateName: test-
+ labels: {}
+ namespace: test-namespace
+spec:
+ pipelineRef:
+ name: simple
+`;
+ expect(namespace).toEqual('test-namespace');
+ expect(yaml.dump(payload)).toEqual(expected);
+});
+it('edit. generate new pipeline run. maximum', () => {
+ const pipelineRun = {
+ apiVersion: 'tekton.dev/v1beta1',
+ kind: 'PipelineRun',
+ metadata: {
+ annotations: {
+ keya: 'valuea',
+ 'kubectl.kubernetes.io/last-applied-configuration':
+ '{"apiVersion": "tekton.dev/v1beta1", "keya": "valuea"}'
+ },
+ labels: {
+ key1: 'valuel',
+ key2: 'value2',
+ 'tekton.dev/pipeline': 'foo',
+ 'tekton.dev/run': 'bar'
+ },
+ name: 'test',
+ namespace: 'test-namespace',
+ uid: '111-233-33',
+ resourceVersion: 'aaaa'
+ },
+ spec: {
+ pipelineRef: {
+ name: 'simple'
+ },
+ params: [{ name: 'param-1' }, { name: 'param-2' }]
+ },
+ status: { startTime: '0' }
+ };
+ const { namespace, payload } = generateNewPipelineRunPayload({
+ pipelineRun,
+ rerun: false
+ });
+ const expected = `apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+ annotations:
+ keya: valuea
+ generateName: test-
+ labels:
+ key1: valuel
+ key2: value2
+ namespace: test-namespace
+spec:
+ pipelineRef:
+ name: simple
+ params:
+ - name: param-1
+ - name: param-2
+`;
+ expect(namespace).toEqual('test-namespace');
+ expect(yaml.dump(payload)).toEqual(expected);
+});
diff --git a/src/containers/CreatePipelineRun/CreatePipelineRun.js b/src/containers/CreatePipelineRun/CreatePipelineRun.js
index 3ca70e9fef..2e0693c6c5 100644
--- a/src/containers/CreatePipelineRun/CreatePipelineRun.js
+++ b/src/containers/CreatePipelineRun/CreatePipelineRun.js
@@ -40,8 +40,10 @@ import {
} from '..';
import {
createPipelineRun,
+ generateNewPipelineRunPayload,
getPipelineRunPayload,
usePipeline,
+ usePipelineRun,
useSelectedNamespace
} from '../../api';
import { isValidLabel } from '../../utils';
@@ -56,7 +58,7 @@ const initialState = {
nodeSelector: [],
params: {},
paramSpecs: [],
- pendingPipelineStatus: '',
+ pipelinePendingStatus: '',
pipelineError: false,
pipelineRef: '',
pipelineRunName: '',
@@ -91,6 +93,11 @@ function CreatePipelineRun() {
return urlSearchParams.get('pipelineName') || '';
}
+ function getPipelineRunName() {
+ const urlSearchParams = new URLSearchParams(location.search);
+ return urlSearchParams.get('pipelineRunName') || '';
+ }
+
function getNamespace() {
const urlSearchParams = new URLSearchParams(location.search);
return (
@@ -426,6 +433,36 @@ function CreatePipelineRun() {
}
if (isYAMLMode()) {
+ const externalPipelineRunName = getPipelineRunName();
+ if (externalPipelineRunName) {
+ const { data: pipelineRunObject, isLoading } = usePipelineRun(
+ {
+ name: externalPipelineRunName,
+ namespace: getNamespace()
+ },
+ { disableWebSocket: true }
+ );
+ let payloadYaml = null;
+ if (pipelineRunObject) {
+ const { payload } = generateNewPipelineRunPayload({
+ pipelineRun: pipelineRunObject,
+ rerun: false
+ });
+ payloadYaml = yaml.dump(payload);
+ }
+ const loadingMessage = intl.formatMessage({
+ id: 'dashboard.pipelineRun.loading',
+ defaultMessage: 'Loading PipelineRun…'
+ });
+
+ return (
+
+ );
+ }
const pipelineRun = getPipelineRunPayload({
labels: labels.reduce((acc, { key, value }) => {
acc[key] = value;
diff --git a/src/containers/CreatePipelineRun/CreatePipelineRun.test.js b/src/containers/CreatePipelineRun/CreatePipelineRun.test.js
index 272da4a9a4..70d44931d9 100644
--- a/src/containers/CreatePipelineRun/CreatePipelineRun.test.js
+++ b/src/containers/CreatePipelineRun/CreatePipelineRun.test.js
@@ -12,7 +12,7 @@ limitations under the License.
*/
import React from 'react';
-import { fireEvent } from '@testing-library/react';
+import { fireEvent, waitFor } from '@testing-library/react';
import { renderWithRouter } from '../../utils/test';
@@ -23,6 +23,7 @@ import * as PipelineRunsAPI from '../../api/pipelineRuns';
import * as PipelinesAPI from '../../api/pipelines';
import * as ServiceAccountsAPI from '../../api/serviceAccounts';
+const submitButton = allByText => allByText('Create')[0];
const pipelines = [
{
metadata: {
@@ -110,14 +111,6 @@ describe('CreatePipelineRun', () => {
expect(queryByDisplayValue(/bar/i)).toBeFalsy();
});
- it('renders yaml mode', () => {
- const { getByRole } = renderWithRouter(, {
- path: '/create',
- route: '/create?mode=yaml'
- });
- expect(getByRole(/textbox/)).toBeTruthy();
- });
-
it('handles onClose event', () => {
jest.spyOn(window.history, 'pushState');
const { getByText } = renderWithRouter();
@@ -133,3 +126,103 @@ describe('CreatePipelineRun', () => {
expect(window.history.pushState).toHaveBeenCalledTimes(2);
});
});
+
+const pipelineRunRawGenerateName = {
+ apiVersion: 'tekton.dev/v1beta1',
+ kind: 'PipelineRun',
+ metadata: {
+ annotations: {},
+ generateName: 'test-pipeline-run-name-',
+ labels: {},
+ namespace: 'test-namespace'
+ },
+ spec: {
+ pipelineSpec: {
+ tasks: [
+ {
+ name: 'hello',
+ taskSpec: {
+ steps: [
+ {
+ image: 'busybox',
+ name: 'echo',
+ script: '#!/bin/ash\necho "Hello World!"\n'
+ }
+ ]
+ }
+ }
+ ]
+ }
+ }
+};
+
+const expectedPipelineRun = `apiVersion: tekton.dev/v1beta1
+kind: PipelineRun
+metadata:
+ name: run-1111111111
+ namespace: test-namespace
+ labels: {}
+spec:
+ pipelineRef:
+ name: ''
+ status: ''`;
+const expectedPipelineRunOneLine = expectedPipelineRun.replace(/\r?\n|\r/g, '');
+const findNameRegexp = /name: run-\S+/;
+describe('CreatePipelineRun yaml mode', () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+
+ jest.spyOn(window.history, 'pushState');
+ });
+
+ it('renders yaml mode with namespace', async () => {
+ jest
+ .spyOn(PipelineRunsAPI, 'createPipelineRunRaw')
+ .mockImplementation(() => Promise.resolve({ data: {} }));
+ jest
+ .spyOn(PipelineRunsAPI, 'usePipelineRun')
+ .mockImplementation(() => ({ data: pipelineRunRawGenerateName }));
+
+ const { getByRole } = renderWithRouter(, {
+ path: '/create',
+ route: '/create?mode=yaml&namespace=test-namespace'
+ });
+
+ expect(getByRole(/textbox/)).toBeTruthy();
+ let actual = getByRole(/textbox/).textContent;
+ actual = actual.replace(findNameRegexp, 'name: run-1111111111');
+ expect(actual.trim()).toEqual(expectedPipelineRunOneLine);
+ });
+
+ it('handle submit. yaml mode with pipelinerun and namespace', async () => {
+ jest
+ .spyOn(PipelineRunsAPI, 'createPipelineRunRaw')
+ .mockImplementation(() => Promise.resolve({ data: {} }));
+ jest
+ .spyOn(PipelineRunsAPI, 'usePipelineRun')
+ .mockImplementation(() => ({ data: pipelineRunRawGenerateName }));
+
+ const { queryAllByText } = renderWithRouter(, {
+ path: '/create',
+ route:
+ '/create?mode=yaml&pipelineRunName=test-pipeline-run-name&namespace=test-namespace'
+ });
+
+ expect(submitButton(queryAllByText)).toBeTruthy();
+
+ fireEvent.click(submitButton(queryAllByText));
+
+ await waitFor(() => {
+ expect(PipelineRunsAPI.createPipelineRunRaw).toHaveBeenCalledTimes(1);
+ });
+ expect(PipelineRunsAPI.createPipelineRunRaw).toHaveBeenCalledWith(
+ expect.objectContaining({
+ namespace: 'test-namespace',
+ payload: pipelineRunRawGenerateName
+ })
+ );
+ await waitFor(() => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(2);
+ });
+ });
+});
diff --git a/src/containers/CreatePipelineRun/YAMLEditor.js b/src/containers/CreatePipelineRun/YAMLEditor.js
index 88f481da43..e5cfbca5d1 100644
--- a/src/containers/CreatePipelineRun/YAMLEditor.js
+++ b/src/containers/CreatePipelineRun/YAMLEditor.js
@@ -16,17 +16,22 @@ import {
Button,
Form,
FormGroup,
- InlineNotification
+ InlineNotification,
+ Loading
} from 'carbon-components-react';
import yaml from 'js-yaml';
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
import CodeMirror from '@uiw/react-codemirror';
import { StreamLanguage } from '@codemirror/language';
import { yaml as codeMirrorYAML } from '@codemirror/legacy-modes/mode/yaml';
import { useNavigate } from 'react-router-dom-v5-compat';
import { createPipelineRunRaw, useSelectedNamespace } from '../../api';
-export function CreateYAMLEditor({ code: initialCode = '' }) {
+export function CreateYAMLEditor({
+ code: initialCode,
+ loading = false,
+ loadingMessage = ''
+}) {
const intl = useIntl();
const navigate = useNavigate();
const { selectedNamespace } = useSelectedNamespace();
@@ -39,6 +44,15 @@ export function CreateYAMLEditor({ code: initialCode = '' }) {
validationErrorMessage: ''
});
+ useEffect(() => {
+ if (!loading) {
+ setState(state => ({
+ ...state,
+ code: initialCode
+ }));
+ }
+ }, [loading]);
+
function validateNamespace(obj) {
if (!obj?.metadata?.namespace) {
return {
@@ -176,14 +190,25 @@ export function CreateYAMLEditor({ code: initialCode = '' }) {
lowContrast
/>
)}
-
-
+
+ <>
+ {loading && (
+
+
+ {loadingMessage}
+
+ )}
+ {!loading && (
+
+ )}
+ >