From eaf97c8ee745ebe660957ab3aaa2a5bf77b324e3 Mon Sep 17 00:00:00 2001
From: Michal Kovarik <mkovarik@redhat.com>
Date: Mar 25 2020 12:49:31 +0000
Subject: Add verification jenkins job


Deploy and test verification job which should be triggered by fedmsg for
c3i-library to verify changes.
1) deploy new Jenkins master to new namespace
2) apply test role to new jenkins master
3) trigger jobs
  a) postmerge
  b) trigger-on-latest-tag
  c) greenwave-promote-to-stage
4) report results to pagure PR

---

diff --git a/c3iaas/Makefile b/c3iaas/Makefile
index 7d0cbfc..92d945b 100644
--- a/c3iaas/Makefile
+++ b/c3iaas/Makefile
@@ -25,24 +25,6 @@ install:
 		echo "[PIPELINE] Pipeline job \"$${job}\" updated" ; \
 	done
 
-create-jenkins-is:
-	$(OC_CMD) import-image jenkins:2 --confirm --scheduled=true \
-		--from=registry.access.redhat.com/openshift3/jenkins-2-rhel7:v3.11
-
-get-jenkins-plugins:
-	$(shell ./get-all-jenkins-plugins.sh)
-
-install-jenkins: create-jenkins-is get-jenkins-plugins
-	$(OC_CMD) new-app --template=jenkins-persistent \
-		-p MEMORY_LIMIT=2Gi \
-		-p VOLUME_CAPACITY=2Gi \
-		-p NAMESPACE=$(shell $(OC_CMD) project -q) \
-		-e INSTALL_PLUGINS=$(shell cat default-jenkins-plugins.txt) \
-		-e JENKINS_JAVA_OVERRIDES="-Dpermissive-script-security.enabled=no_security"
-
-remove-jenkins:
-	$(OC_CMD) delete -l app=jenkins-persistent all,sa,rolebindings,pvc
-
 uninstall:
 	@for job in $(JOBS); do \
 		echo "[PIPELINE] Deleting pipeline job \"$${job}\"..." ; \
@@ -51,4 +33,4 @@ uninstall:
 			--param-file ./$(JOBS_DIR)/$${job}.env | $(OC_CMD) delete -f -; \
 		echo "[PIPELINE] Pipeline job \"$${job}\" deleted" ; \
 	done
-.PHONY: help install install-jenkins remove-jenkins uninstall
+.PHONY: help install uninstall
diff --git a/c3iaas/get-all-jenkins-plugins.sh b/c3iaas/get-all-jenkins-plugins.sh
deleted file mode 100755
index f87a29f..0000000
--- a/c3iaas/get-all-jenkins-plugins.sh
+++ /dev/null
@@ -1,5 +0,0 @@
-#!/bin/bash
-
-curl https://raw.githubusercontent.com/openshift/jenkins/master/2/contrib/openshift/base-plugins.txt | grep -v "#" |grep -v '^$' | sed 's/:.*/:latest/'| tr '\n' ',' > default-jenkins-plugins.txt
-
-cat jenkins-plugins.txt | tr '\n' ',' >> default-jenkins-plugins.txt
diff --git a/c3iaas/jenkins-plugins.txt b/c3iaas/jenkins-plugins.txt
deleted file mode 100644
index 0414409..0000000
--- a/c3iaas/jenkins-plugins.txt
+++ /dev/null
@@ -1,6 +0,0 @@
-script-security:latest
-permissive-script-security:latest
-timestamper:latest
-http_request:latest
-antisamy-markup-formatter:latest
-update-sites-manager:latest
diff --git a/roles/c3i/templates/default-agent.groovy b/roles/c3i/templates/default-agent.groovy
index 00fa9a3..dcf8f7e 100644
--- a/roles/c3i/templates/default-agent.groovy
+++ b/roles/c3i/templates/default-agent.groovy
@@ -9,7 +9,7 @@ agent {
     kind: Pod
     metadata:
       labels:
-        app: "${env.JOB_BASE_NAME}"
+        app: "${env.JOB_BASE_NAME.take(62).replaceAll(/-$/,'')}"
         factory2-pipeline-build-number: "${env.BUILD_NUMBER}"
     spec:
       containers:
diff --git a/setup/create-secret-key b/setup/create-secret-key
new file mode 100755
index 0000000..37a5018
--- /dev/null
+++ b/setup/create-secret-key
@@ -0,0 +1,13 @@
+#!/bin/bash -e
+
+if [ $# -ne 2 ]; then
+  echo "Provide key name and value"
+  exit 1
+fi
+
+NAME=$1
+VALUE=$2
+
+oc delete secret $NAME --ignore-not-found=true
+oc create secret generic $NAME --from-literal=secrettext=$VALUE
+oc label secret $NAME credential.sync.jenkins.openshift.io=true
diff --git a/setup/install-jenkins b/setup/install-jenkins
new file mode 100755
index 0000000..e4488f6
--- /dev/null
+++ b/setup/install-jenkins
@@ -0,0 +1,26 @@
+#!/bin/bash -e
+
+
+while [[ "$#" -gt 0 ]]; do case $1 in
+  --jcasc) JCASC="$2"; shift;;
+  *) echo "Unknown parameter passed: $1"; exit 1;;
+esac; shift; done
+
+oc import-image jenkins:2 --confirm --scheduled=true \
+  --from=registry.access.redhat.com/openshift3/jenkins-2-rhel7:v3.11
+
+# get jenkins plugins
+PLUGINS=$(curl https://raw.githubusercontent.com/openshift/jenkins/master/2/contrib/openshift/base-plugins.txt | grep -v "#" |grep -v '^$' | sed 's/:.*/:latest/')
+PLUGINS=$(echo -e "$PLUGINS\n$(cat jenkins-plugins.txt)" | sort -u | tr '\n' ',')
+
+cat jenkins-plugins.txt | tr '\n' ',' >> default-jenkins-plugins.txt
+
+oc create configmap openshift-jenkins-login-plugin-config --from-literal Overall-Administer=view
+
+oc new-app --file=jenkins-template.yaml \
+        -p MEMORY_LIMIT=2Gi \
+        -p VOLUME_CAPACITY=2Gi \
+        -p NAMESPACE=$(oc project -q) \
+        -e INSTALL_PLUGINS=$PLUGINS \
+        -e JENKINS_JAVA_OVERRIDES="-Dpermissive-script-security.enabled=no_security" \
+        ${JCASC:+--env CASC_JENKINS_CONFIG=$JCASC}
diff --git a/setup/jcasc.yml b/setup/jcasc.yml
new file mode 100644
index 0000000..e3108be
--- /dev/null
+++ b/setup/jcasc.yml
@@ -0,0 +1,7 @@
+unclassified:
+  globalCIConfiguration:
+    configs:
+    - fedMsgMessagingProvider:
+        hubAddr: "tcp://hub.fedoraproject.org:9940"
+        name: "fedmsg"
+        pubAddr: "tcp://hub.fedoraproject.org:9940"
diff --git a/setup/jenkins-plugins.txt b/setup/jenkins-plugins.txt
new file mode 100644
index 0000000..0959599
--- /dev/null
+++ b/setup/jenkins-plugins.txt
@@ -0,0 +1,8 @@
+permissive-script-security:latest
+timestamper:latest
+http_request:latest
+antisamy-markup-formatter:latest
+update-sites-manager:latest
+jms-messaging:latest
+configuration-as-code:latest
+rebuild:latest
diff --git a/setup/jenkins-template.yaml b/setup/jenkins-template.yaml
new file mode 100644
index 0000000..8745711
--- /dev/null
+++ b/setup/jenkins-template.yaml
@@ -0,0 +1,219 @@
+apiVersion: template.openshift.io/v1
+kind: Template
+labels:
+  app: jenkins-persistent
+  template: jenkins-persistent-template
+message: A Jenkins service has been created in your project.  Log into Jenkins with
+  your OpenShift account.  The tutorial at https://github.com/openshift/origin/blob/master/examples/jenkins/README.md
+  contains more information about using this template.
+metadata:
+  annotations:
+    description: |-
+      Jenkins service, with persistent storage.
+
+      NOTE: You must have persistent volumes available in your cluster to use this template.
+    iconClass: icon-jenkins
+    openshift.io/display-name: Jenkins
+    openshift.io/documentation-url: https://docs.openshift.org/latest/using_images/other_images/jenkins.html
+    openshift.io/long-description: This template deploys a Jenkins server capable
+      of managing OpenShift Pipeline builds and supporting OpenShift-based oauth login.
+    openshift.io/provider-display-name: Red Hat, Inc.
+    openshift.io/support-url: https://access.redhat.com
+    tags: instant-app,jenkins
+  name: jenkins-persistent
+objects:
+- apiVersion: v1
+  kind: Route
+  metadata:
+    annotations:
+      haproxy.router.openshift.io/timeout: 4m
+      template.openshift.io/expose-uri: http://{.spec.host}{.spec.path}
+    name: ${JENKINS_SERVICE_NAME}
+  spec:
+    tls:
+      insecureEdgeTerminationPolicy: Redirect
+      termination: edge
+    to:
+      kind: Service
+      name: ${JENKINS_SERVICE_NAME}
+- apiVersion: v1
+  kind: PersistentVolumeClaim
+  metadata:
+    name: ${JENKINS_SERVICE_NAME}
+  spec:
+    accessModes:
+    - ReadWriteOnce
+    resources:
+      requests:
+        storage: ${VOLUME_CAPACITY}
+- apiVersion: v1
+  kind: DeploymentConfig
+  metadata:
+    annotations:
+      template.alpha.openshift.io/wait-for-ready: "true"
+    name: ${JENKINS_SERVICE_NAME}
+  spec:
+    replicas: 1
+    selector:
+      name: ${JENKINS_SERVICE_NAME}
+    strategy:
+      type: Recreate
+    template:
+      metadata:
+        labels:
+          name: ${JENKINS_SERVICE_NAME}
+      spec:
+        containers:
+        - capabilities: {}
+          env:
+          - name: OPENSHIFT_ENABLE_OAUTH
+            value: ${ENABLE_OAUTH}
+          - name: OPENSHIFT_ENABLE_REDIRECT_PROMPT
+            value: "true"
+          - name: DISABLE_ADMINISTRATIVE_MONITORS
+            value: ${DISABLE_ADMINISTRATIVE_MONITORS}
+          - name: KUBERNETES_MASTER
+            value: https://kubernetes.default:443
+          - name: KUBERNETES_TRUST_CERTIFICATES
+            value: "true"
+          - name: JENKINS_SERVICE_NAME
+            value: ${JENKINS_SERVICE_NAME}
+          - name: JNLP_SERVICE_NAME
+            value: ${JNLP_SERVICE_NAME}
+          - name: ENABLE_FATAL_ERROR_LOG_FILE
+            value: ${ENABLE_FATAL_ERROR_LOG_FILE}
+          image: ' '
+          imagePullPolicy: IfNotPresent
+          livenessProbe:
+            failureThreshold: 2
+            httpGet:
+              path: /login
+              port: 8080
+            initialDelaySeconds: 420
+            periodSeconds: 360
+            timeoutSeconds: 240
+          name: jenkins
+          readinessProbe:
+            httpGet:
+              path: /login
+              port: 8080
+            initialDelaySeconds: 3
+            timeoutSeconds: 240
+          resources:
+            limits:
+              memory: ${MEMORY_LIMIT}
+              cpu: 2
+          volumeMounts:
+          - mountPath: /var/lib/jenkins
+            name: ${JENKINS_SERVICE_NAME}-data
+        dnsPolicy: ClusterFirst
+        restartPolicy: Always
+        serviceAccountName: ${JENKINS_SERVICE_NAME}
+        volumes:
+        - name: ${JENKINS_SERVICE_NAME}-data
+          persistentVolumeClaim:
+            claimName: ${JENKINS_SERVICE_NAME}
+    triggers:
+    - imageChangeParams:
+        automatic: true
+        containerNames:
+        - jenkins
+        from:
+          kind: ImageStreamTag
+          name: ${JENKINS_IMAGE_STREAM_TAG}
+          namespace: ${NAMESPACE}
+        lastTriggeredImage: ""
+      type: ImageChange
+    - type: ConfigChange
+- apiVersion: v1
+  kind: ServiceAccount
+  metadata:
+    annotations:
+      serviceaccounts.openshift.io/oauth-redirectreference.jenkins: '{"kind":"OAuthRedirectReference","apiVersion":"v1","reference":{"kind":"Route","name":"${JENKINS_SERVICE_NAME}"}}'
+    name: ${JENKINS_SERVICE_NAME}
+- apiVersion: v1
+  groupNames: null
+  kind: RoleBinding
+  metadata:
+    name: ${JENKINS_SERVICE_NAME}_edit
+  roleRef:
+    name: edit
+  subjects:
+  - kind: ServiceAccount
+    name: ${JENKINS_SERVICE_NAME}
+- apiVersion: v1
+  kind: Service
+  metadata:
+    name: ${JNLP_SERVICE_NAME}
+  spec:
+    ports:
+    - name: agent
+      nodePort: 0
+      port: 50000
+      protocol: TCP
+      targetPort: 50000
+    selector:
+      name: ${JENKINS_SERVICE_NAME}
+    sessionAffinity: None
+    type: ClusterIP
+- apiVersion: v1
+  kind: Service
+  metadata:
+    annotations:
+      service.alpha.openshift.io/dependencies: '[{"name": "${JNLP_SERVICE_NAME}",
+        "namespace": "", "kind": "Service"}]'
+      service.openshift.io/infrastructure: "true"
+    name: ${JENKINS_SERVICE_NAME}
+  spec:
+    ports:
+    - name: web
+      nodePort: 0
+      port: 80
+      protocol: TCP
+      targetPort: 8080
+    selector:
+      name: ${JENKINS_SERVICE_NAME}
+    sessionAffinity: None
+    type: ClusterIP
+parameters:
+- description: The name of the OpenShift Service exposed for the Jenkins container.
+  displayName: Jenkins Service Name
+  name: JENKINS_SERVICE_NAME
+  value: jenkins
+- description: The name of the service used for master/slave communication.
+  displayName: Jenkins JNLP Service Name
+  name: JNLP_SERVICE_NAME
+  value: jenkins-jnlp
+- description: Whether to enable OAuth OpenShift integration. If false, the static
+    account 'admin' will be initialized with the password 'password'.
+  displayName: Enable OAuth in Jenkins
+  name: ENABLE_OAUTH
+  value: "true"
+- description: Maximum amount of memory the container can use.
+  displayName: Memory Limit
+  name: MEMORY_LIMIT
+  value: 512Mi
+- description: Volume space available for data, e.g. 512Mi, 2Gi.
+  displayName: Volume Capacity
+  name: VOLUME_CAPACITY
+  required: true
+  value: 1Gi
+- description: The OpenShift Namespace where the Jenkins ImageStream resides.
+  displayName: Jenkins ImageStream Namespace
+  name: NAMESPACE
+  value: openshift
+- description: Whether to perform memory intensive, possibly slow, synchronization
+    with the Jenkins Update Center on start.  If true, the Jenkins core update monitor
+    and site warnings monitor are disabled.
+  displayName: Disable memory intensive administrative monitors
+  name: DISABLE_ADMINISTRATIVE_MONITORS
+  value: "false"
+- description: Name of the ImageStreamTag to be used for the Jenkins image.
+  displayName: Jenkins ImageStreamTag
+  name: JENKINS_IMAGE_STREAM_TAG
+  value: jenkins:2
+- description: When a fatal error occurs, an error log is created with information
+    and the state obtained at the time of the fatal error.
+  displayName: Fatal Error Log File
+  name: ENABLE_FATAL_ERROR_LOG_FILE
+  value: "false"
diff --git a/verification/c3i-role-verification.Jenkinsfile b/verification/c3i-role-verification.Jenkinsfile
new file mode 100644
index 0000000..cf89019
--- /dev/null
+++ b/verification/c3i-role-verification.Jenkinsfile
@@ -0,0 +1,252 @@
+import static org.apache.commons.lang.StringEscapeUtils.escapeHtml;
+
+library identifier: "c3i@master", changelog: false,
+  retriever: modernSCM([$class: 'GitSCMSource', remote: "https://pagure.io/c3i-library.git"])
+pipeline {
+
+  {% include 'default-agent.groovy' %}
+
+  options {
+    timestamps()
+    timeout(time: 30, unit: 'MINUTES')
+  }
+  environment {
+    SERVICE_ACCOUNT_TOKEN = readFile(file: '/run/secrets/kubernetes.io/serviceaccount/token').trim()
+    // needed by c3i pagure client
+    TRIGGER_NAMESPACE = readFile('/run/secrets/kubernetes.io/serviceaccount/namespace').trim()
+    PAGURE_REPO_NAME = env.GIT_REPO.split('/')[3..-1].join('/').replace('forks/', '').replaceAll(/.git$/, '')
+    PAGURE_URL = env.GIT_REPO.split('/')[0..2].join('/')
+    PAGURE_API = "${env.PAGURE_URL}/api/0"
+    PAGURE_REPO_IS_FORK = "${env.GIT_REPO.contains('/forks/') ? 'true': 'false'}"
+  }
+  triggers {
+    ciBuildTrigger(
+      noSquash: true,
+      providerList: [
+        fedmsgSubscriber(
+          name: params.MESSAGING_FEDMSG_PROVIDER,
+          overrides: [topic: "io.pagure.prod.pagure.pull-request.new"],
+          checks: [
+            [field: '$.pullrequest.project.url_path', expectedValue: params.GIT_REPO.split('/')[3..-1].join('/').replace('forks/', 'fork/').replaceAll(/.git$/, '')],
+            [field: '$.pullrequest.branch', expectedValue: params.GIT_MAIN_BRANCH],
+          ]
+        ),
+        fedmsgSubscriber(
+          name: params.MESSAGING_FEDMSG_PROVIDER,
+          overrides: [topic: "io.pagure.prod.pagure.pull-request.update"],
+          checks: [
+            [field: '$.pullrequest.project.url_path', expectedValue: params.GIT_REPO.split('/')[3..-1].join('/').replace('forks/', 'fork/').replaceAll(/.git$/, '')],
+            [field: '$.pullrequest.branch', expectedValue: params.GIT_MAIN_BRANCH],
+          ]
+        ),
+        fedmsgSubscriber(
+          name: params.MESSAGING_FEDMSG_PROVIDER,
+          overrides: [topic: "io.pagure.prod.pagure.pull-request.reopened"],
+          checks: [
+            [field: '$.pullrequest.project.url_path', expectedValue: params.GIT_REPO.split('/')[3..-1].join('/').replace('forks/', 'fork/').replaceAll(/.git$/, '')],
+            [field: '$.pullrequest.branch', expectedValue: params.GIT_MAIN_BRANCH],
+          ]
+        ),
+        fedmsgSubscriber(
+          name: params.MESSAGING_FEDMSG_PROVIDER,
+          overrides: [topic: "io.pagure.prod.pagure.pull-request.rebased"],
+          checks: [
+            [field: '$.pullrequest.project.url_path', expectedValue: params.GIT_REPO.split('/')[3..-1].join('/').replace('forks/', 'fork/').replaceAll(/.git$/, '')],
+            [field: '$.pullrequest.branch', expectedValue: params.GIT_MAIN_BRANCH],
+          ]
+        ),
+      ]
+    )
+  }
+  stages {
+    stage('Proceeding CI_MESSAGE') {
+      when {
+        expression { env.CI_MESSAGE }
+      }
+      steps {
+        script {
+          def message = readJSON text: params.CI_MESSAGE
+          env.GIT_REPO_REF = "pull/${message.pullrequest.id}/head"
+        }
+      }
+    }
+    stage('Update Build Info') {
+      steps {
+        script {
+          if (!env.GIT_REPO_REF) {
+            error("This build is not started by a CI message and GIT_REPO_REF is empty. Only configurations were done.")
+          }
+
+          c3i.clone(repo: params.GIT_REPO,
+            branch: env.GIT_REPO_REF)
+          env.GIT_COMMIT = sh(returnStdout: true, script: 'git rev-parse HEAD').trim()
+          echo "Build ${env.GIT_REPO_REF}, commit=${env.GIT_COMMIT}"
+
+          // Set friendly display name and description
+          def pagure_repo_home = env.GIT_REPO.replace('/forks/', '/fork/').replaceAll(/.git$/,'')
+          // Is the current branch a pull-request? If no, env.PR_NO will be empty.
+          env.PR_NO = env.GIT_REPO_REF.split('/')[1] // return X from pull/X/head
+          env.PR_URL = "${pagure_repo_home}/pull-request/${env.PR_NO}"
+          echo "Building PR #${env.PR_NO}: ${env.PR_URL}"
+          // NOTE: Old versions of OpenShift Client Jenkins plugin are buggy to handle arguments
+          // with special bash characters (like whitespaces, #, etc).
+          // https://bugzilla.redhat.com/show_bug.cgi?id=1625518
+          currentBuild.displayName = "PR#${env.PR_NO}"
+          // To enable HTML syntax in build description, go to `Jenkins/Global Security/Markup Formatter` and select 'Safe HTML'.
+          def pagureLink = """<a href="${env.PR_URL}">${currentBuild.displayName}</a>"""
+          try {
+            def prInfo = pagure.getPR(env.PR_NO)
+            pagureLink = """<a href="${env.PR_URL}">PR#${env.PR_NO}: ${escapeHtml(prInfo.title)}</a>"""
+            // set PR status to Pending
+            if (params.PAGURE_API_KEY_SECRET_NAME)
+              pagure.setBuildStatusOnPR(null, 'Building...')
+          } catch (Exception e) {
+            echo "Error using pagure API: ${e}"
+          }
+          currentBuild.description = pagureLink
+          def prInfo = pagure.getPR(env.PR_NO)
+          env.PAGURE_REQUEST_BRANCH = prInfo.branch_from
+          env.PAGURE_REQUEST_REPO = "${env.PAGURE_URL}/${prInfo.repo_from.fullname}"
+        }
+      }
+    }
+    stage('Allocate C3IaaS project') {
+      steps {
+        script {
+          env.PIPELINE_ID = "c3i-verification-pr-${env.PR_NO}-git${env.GIT_COMMIT.take(8)}-${currentBuild.id}"
+          echo "Requesting new OpenShift project ${env.PIPELINE_ID}..."
+          openshift.withCluster() {
+            openshift.withProject(params.C3IAAS_NAMESPACE) {
+              c3i.buildAndWait(script: this, objs: "bc/${params.C3IAAS_JOB_NAME}",
+                '-e', "PROJECT_NAME=${env.PIPELINE_ID}",
+                '-e', "ADMIN_GROUPS=system:serviceaccounts:${env.TRIGGER_NAMESPACE},system:serviceaccounts:${env.C3IAAS_NAMESPACE}",
+                '-e', "LIFETIME_IN_MINUTES=60"
+              )
+            }
+          }
+        }
+      }
+      post {
+        success {
+          echo "Allocated project ${env.PIPELINE_ID}"
+        }
+        failure {
+          echo "Failed to allocate ${env.PIPELINE_ID} project"
+        }
+      }
+    }
+    stage('Copy Secrets') {
+      steps {
+        script {
+          openshift.withCluster() {
+            factorySecret = openshift.selector('secret', 'factory2-c3i-verification-secret').object(exportable:true)
+            openshift.withProject(env.PIPELINE_ID) {
+              openshift.create(factorySecret)
+            }
+          }
+        }
+      }
+    }
+    stage('Install Jenkins master') {
+      steps {
+        script {
+          sh "oc project ${env.PIPELINE_ID}"
+          dir('setup') {
+             sh "./install-jenkins --jcasc ${env.PAGURE_REQUEST_REPO.replace('/forks/', '/fork/')}/raw/${env.PAGURE_REQUEST_BRANCH}/f/setup/jcasc.yml"
+          }
+          openshift.withCluster() {
+            openshift.withProject(env.PIPELINE_ID) {
+               c3i.waitForPods(script: this, objs: [name: "jenkins"], num: 1)
+            }
+          }
+        }
+      }
+    }
+    stage('Setup test project') {
+      steps {
+        script {
+          dir('verification/test-project') {
+            sh 'ln -s ../../roles roles'
+            sh "ansible-playbook deploy.yml -e c3i_ocp_namespace=${env.PIPELINE_ID} -e c3i_lib_branch=${env.PAGURE_REQUEST_BRANCH} -e c3i_lib_url=${env.PAGURE_REQUEST_REPO} -e c3i_git_repo=${env.PAGURE_REQUEST_REPO} -e c3i_git_main_branch=${env.PAGURE_REQUEST_BRANCH}"
+          }
+        }
+      }
+    }
+    stage('postmerge') {
+      steps {
+        script {
+          openshift.withCluster() {
+            openshift.withProject(env.PIPELINE_ID) {
+              c3i.buildAndWait(script: this, objs: "bc/c3i-library-test-postmerge",
+               '-e', "IMAGE=registry.access.redhat.com/rhscl/postgresql-95-rhel7:latest",
+               '-e', "IMAGE_TAG=latest-PR${env.PR_NO}-${env.BUILD_NUMBER}",
+               '-e', "PAGURE_API_KEY_SECRET_NAME=", // disable for pagure communication
+               '-e', "GIT_REPO_REF=${env.PAGURE_REQUEST_BRANCH}",
+               '-e', "LIFETIME_IN_MINUTES=30"
+              )
+            }
+          }
+        }
+      }
+    }
+    stage('Parallel trigger') {
+      steps {
+        script {
+          openshift.withCluster() {
+            openshift.withProject(env.PIPELINE_ID) {
+              parallel(
+                "trigger-on-latest-tag": {
+                  c3i.buildAndWait(script: this, objs: "bc/c3i-library-test-trigger-on-latest-tag",
+                   '-e', "IMAGE=quay.io/factory2/c3i-library-test:latest-PR${env.PR_NO}-${env.BUILD_NUMBER}",
+                   '-e', "TARGET_TAG=stage-PR${env.PR_NO}-${env.BUILD_NUMBER}"
+                  )
+                },
+                "greenwave-promote-to-stage": {
+                  c3i.buildAndWait(script: this, objs: "bc/c3i-library-test-greenwave-promote-to-stage",
+                   '-e', "IMAGE=quay.io/factory2/c3i-library-test:latest-PR${env.PR_NO}-${env.BUILD_NUMBER}",
+                   '-e', "TARGET_TAG=stage-PR${env.PR_NO}-${env.BUILD_NUMBER}"
+                  )
+                }
+              )
+            }
+          }
+        }
+      }
+    }
+  }
+  post {
+    success {
+      script {
+        if (env.PAGURE_API_KEY_SECRET_NAME && env.PR_NO) {
+          try {
+            pagure.setBuildStatusOnPR(100, 'Build passed.')
+            echo "Updated PR #${env.PR_NO} status to PASS."
+          } catch (e) {
+            echo "Error updating PR #${env.PR_NO} status to PASS: ${e}"
+          }
+        }
+      }
+    }
+    failure {
+      script {
+        if (params.PAGURE_API_KEY_SECRET_NAME && env.PR_NO) {
+          try {
+            pagure.setBuildStatusOnPR(0, 'Build failed.')
+            echo "Updated PR #${env.PR_NO} status to FAILURE."
+          } catch (e) {
+            echo "Error updating PR #${env.PR_NO} status to FAILURE: ${e}"
+          }
+          try {
+            pagure.commentOnPR("""
+            Build ${env.GIT_COMMIT} [FAILED](${env.BUILD_URL})!
+            Rebase or make new commits to rebuild.
+            """.stripIndent(), env.PR_NO)
+            echo "Comment made."
+          } catch (e) {
+            echo "Error making a comment on PR #${env.PR_NO}: ${e}"
+          }
+        }
+      }
+    }
+  }
+}
diff --git a/verification/c3i-role-verification.yml b/verification/c3i-role-verification.yml
new file mode 100644
index 0000000..adc1f41
--- /dev/null
+++ b/verification/c3i-role-verification.yml
@@ -0,0 +1,38 @@
+kind: "BuildConfig"
+apiVersion: "v1"
+metadata:
+  name: c3i-role-verification
+  labels:
+    app: c3i-role-verification
+spec:
+  runPolicy: "Serial"
+  completionDeadlineSeconds: 1800
+  strategy:
+    type: JenkinsPipeline
+    jenkinsPipelineStrategy:
+      env:
+      - name: GIT_REPO
+        value: https://pagure.io/c3i-library.git
+      - name: GIT_REPO_REF
+        value:
+      - name: GIT_MAIN_BRANCH
+        value: master
+      - name: OPENSHIFT_CLOUD_NAME
+        value: openshift
+      - name: JENKINS_AGENT_IMAGE
+        value: docker-registry.upshift.redhat.com/factory2/pipeline-jenkins-agent:latest
+      - name: MESSAGING_FEDMSG_PROVIDER
+        value: fedmsg
+      - name: PAGURE_API_KEY_SECRET_NAME
+        value: pagure-api-key-c3i-library
+      - name: C3IAAS_NAMESPACE
+        value: c3i
+      - name: C3IAAS_JOB_NAME
+        value: c3iaas-request-project
+      - name: PAAS_DOMAIN
+        value: cloud.paas.psi.redhat.com
+      - name: CI_MESSAGE
+      - name: JENKINS_AGENT_SERVICE_ACCOUNT
+        value: jenkins
+      jenkinsfile: |
+        {% filter indent(width=10) %}{% include "c3i-role-verification.Jenkinsfile" %}{% endfilter %}
diff --git a/verification/default-agent.groovy b/verification/default-agent.groovy
new file mode 100644
index 0000000..dcf8f7e
--- /dev/null
+++ b/verification/default-agent.groovy
@@ -0,0 +1,29 @@
+agent {
+  kubernetes {
+    cloud "${params.OPENSHIFT_CLOUD_NAME}"
+    label "jenkins-slave-${UUID.randomUUID().toString()}"
+    serviceAccount "${params.JENKINS_AGENT_SERVICE_ACCOUNT}"
+    defaultContainer 'jnlp'
+    yaml """
+    apiVersion: v1
+    kind: Pod
+    metadata:
+      labels:
+        app: "${env.JOB_BASE_NAME.take(62).replaceAll(/-$/,'')}"
+        factory2-pipeline-build-number: "${env.BUILD_NUMBER}"
+    spec:
+      containers:
+      - name: jnlp
+        image: "${params.JENKINS_AGENT_IMAGE}"
+        imagePullPolicy: Always
+        tty: true
+        resources:
+          requests:
+            memory: 512Mi
+            cpu: 200m
+          limits:
+            memory: 768Mi
+            cpu: 300m
+    """
+  }
+}
diff --git a/verification/test-project/Dockerfile b/verification/test-project/Dockerfile
new file mode 100644
index 0000000..6ef00f5
--- /dev/null
+++ b/verification/test-project/Dockerfile
@@ -0,0 +1,3 @@
+FROM registry.access.redhat.com/rhscl/postgresql-95-rhel7:latest
+
+LABEL quay.expires-after=2h
diff --git a/verification/test-project/c3i-role-vars.yml b/verification/test-project/c3i-role-vars.yml
new file mode 100644
index 0000000..88db584
--- /dev/null
+++ b/verification/test-project/c3i-role-vars.yml
@@ -0,0 +1,18 @@
+c3i_component: c3i-library-test
+c3i_build_and_test_snippet: snippets/build-and-test.groovy
+c3i_integration_test_snippet: snippets/full-integration-test.groovy
+
+c3i_mail_address:
+
+c3i_lib_url:
+c3i_lib_branch:
+
+c3i_git_repo:
+c3i_git_main_branch:
+
+c3i_jenkins_agent_image: "{{ c3i_workflow_jenkins_image }}"
+
+# disable build of jenkins agent
+c3i_jenkins_agent_buildconfig_dockerfile:
+
+c3i_container_registry_credentials: factory2-c3i-verification-secret
diff --git a/verification/test-project/container-template.yaml b/verification/test-project/container-template.yaml
new file mode 100644
index 0000000..deb6579
--- /dev/null
+++ b/verification/test-project/container-template.yaml
@@ -0,0 +1,74 @@
+apiVersion: v1
+kind: Template
+metadata:
+  name: container-template
+labels:
+  template: "container-template"
+parameters:
+- name: NAME
+  displayName: Short unique identifier for the templated instances.
+  required: true
+  value: "container"
+- name: GIT_REPO
+  displayName: Git repo URL
+  description: Default Git repo URL in which to run dev tests against
+  required: true
+  value: https://pagure.io/c3i-library.git
+- name: GIT_REF
+  displayName: Git repo ref
+  description: Default Git repo ref in which to run dev tests against
+  required: true
+  value: master
+- name: IMAGESTREAM_NAME
+  displayName: ImageStream name of the resulting image
+  required: true
+  value: test
+- name: IMAGE_TAG
+  displayName: Tag of resulting image
+  required: true
+  value: latest
+- name: IMAGESTREAM_NAMESPACE
+  displayName: Namespace of ImageStream for container images
+  required: false
+objects:
+- apiVersion: v1
+  kind: ImageStream
+  metadata:
+    name: "${IMAGESTREAM_NAME}"
+    labels:
+      app: "${NAME}"
+- apiVersion: v1
+  kind: ImageStream
+  metadata:
+    name: "${IMAGESTREAM_NAME}"
+    labels:
+      app: "${NAME}"
+- kind: "BuildConfig"
+  apiVersion: "v1"
+  metadata:
+    name: "${NAME}"
+    labels:
+      app: "${NAME}"
+  spec:
+    runPolicy: "Parallel"
+    completionDeadlineSeconds: 1800
+    strategy:
+      dockerStrategy:
+        forcePull: true
+        dockerfilePath: verification/test-project/Dockerfile
+    resources:
+      requests:
+        memory: "768Mi"
+        cpu: "300m"
+      limits:
+       memory: "1Gi"
+       cpu: "500m"
+    source:
+      git:
+        uri: "${GIT_REPO}"
+        ref: "${GIT_REF}"
+    output:
+      to:
+        kind: "ImageStreamTag"
+        name: "${IMAGESTREAM_NAME}:${IMAGE_TAG}"
+        namespace: "${IMAGESTREAM_NAMESPACE}"
diff --git a/verification/test-project/deploy.yml b/verification/test-project/deploy.yml
new file mode 100644
index 0000000..6d014d8
--- /dev/null
+++ b/verification/test-project/deploy.yml
@@ -0,0 +1,9 @@
+- name: Deplomyent playbook
+  hosts: localhost
+  vars_files:
+    - c3i-role-vars.yml
+  tasks:
+    - include_role:
+        name: c3i
+      tags:
+        - validate
diff --git a/verification/test-project/snippets/build-and-test.groovy b/verification/test-project/snippets/build-and-test.groovy
new file mode 100644
index 0000000..0756e22
--- /dev/null
+++ b/verification/test-project/snippets/build-and-test.groovy
@@ -0,0 +1,37 @@
+stage('Build container') {
+  environment {
+    BUILDCONFIG_INSTANCE_ID = "verification-${currentBuild.id}-${UUID.randomUUID().toString().substring(0,7)}"
+    TEMP_TAG = "verification-${currentBuild.id}"
+  }
+  steps {
+    script {
+      openshift.withCluster() {
+        openshift.withProject(env.PIPELINE_ID) {
+          def template = readYaml file: 'verification/test-project/container-template.yaml'
+          def processed = openshift.process(template,
+            "-p", "NAME=${env.BUILDCONFIG_INSTANCE_ID}",
+            '-p', "GIT_REPO=${params.GIT_REPO}",
+            // A pull-request branch, like pull/123/head, cannot be built with commit ID
+            // because refspec cannot be customized in an OpenShift build .
+            '-p', "GIT_REF=${env.PR_NO ? env.GIT_REPO_REF : env.GIT_COMMIT}",
+            '-p', "IMAGE_TAG=${env.TEMP_TAG}",
+            '-p', "IMAGESTREAM_NAME=c3i-library-test",
+            '-p', "IMAGESTREAM_NAMESPACE=${env.PIPELINE_ID}",
+          )
+          def build = c3i.buildAndWait(script: this, objs: processed)
+          echo 'Container build succeeds.'
+          def imagestream = openshift.selector('is', ['app': env.BUILDCONFIG_INSTANCE_ID]).object()
+          env.RESULTING_IMAGE_REPOS = imagestream.status.dockerImageRepository
+          env.RESULTING_TAG = env.TEMP_TAG
+        }
+      }
+    }
+  }
+}
+stage('Tests') {
+  steps {
+    script {
+      echo "Passed"
+    }
+  }
+}
diff --git a/verification/test-project/snippets/full-integration-test.groovy b/verification/test-project/snippets/full-integration-test.groovy
new file mode 100644
index 0000000..a47caea
--- /dev/null
+++ b/verification/test-project/snippets/full-integration-test.groovy
@@ -0,0 +1,8 @@
+stage('Tests') {
+  steps {
+    script {
+      sh "skopeo inspect docker://${env.IMAGE}"
+      echo "Passed"
+    }
+  }
+}

