Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multi cluster promotion #19

Merged
merged 45 commits into from
Feb 28, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c15c4a8
Initial commit of multi-cluster pipeline so I can start testing
etsauer Feb 16, 2018
2618826
Renaming directory
etsauer Feb 16, 2018
4796528
Missing , in jenkinsfile
etsauer Feb 16, 2018
3ed0a39
Check out scm from parameter
etsauer Feb 16, 2018
f13d310
Moving secret grab out of skopeo image
etsauer Feb 16, 2018
26466e9
Removing input fields
etsauer Feb 16, 2018
d0ddbf8
Unescaping registry var
etsauer Feb 16, 2018
ff5147c
Working pipeline
etsauer Feb 19, 2018
ec39777
Fix formatting issue in readme
etsauer Feb 19, 2018
28c9f8a
Initial conversion to declarative and openshift/jenkins-client-plugin…
etsauer Feb 20, 2018
9acfa4c
Remove some hard coded values
etsauer Feb 20, 2018
6d8ad3c
Echo app name
etsauer Feb 20, 2018
80a22a5
Switching to .project()
etsauer Feb 20, 2018
3b19881
Switching to .project()
etsauer Feb 20, 2018
42bbc79
Switching back to NAMESPACE
etsauer Feb 20, 2018
78eb98c
Refactor skopeo promotion
etsauer Feb 20, 2018
24e1b38
Test Jenkinsfile
etsauer Feb 20, 2018
de3db9d
Test Jenkinsfile
etsauer Feb 20, 2018
379ed8d
Test Jenkinsfile
etsauer Feb 20, 2018
3311869
Test Jenkinsfile
etsauer Feb 20, 2018
e7a8536
Test Jenkinsfile
etsauer Feb 20, 2018
f0777b9
Test Jenkinsfile
etsauer Feb 20, 2018
43a1507
Test Jenkinsfile
etsauer Feb 22, 2018
af79994
Enable promotion step
etsauer Feb 22, 2018
dcc0f69
Enable promotion step
etsauer Feb 22, 2018
29f0251
Enable promotion step
etsauer Feb 22, 2018
a5e85c3
Enable promotion step
etsauer Feb 22, 2018
d7ea6e9
Enable promotion step
etsauer Feb 22, 2018
31a1177
Enable promotion step
etsauer Feb 22, 2018
a700044
Enable promotion step
etsauer Feb 22, 2018
ac7b48f
Enable promotion step
etsauer Feb 22, 2018
07a445f
Enable promotion step
etsauer Feb 22, 2018
2f958d7
Enable promotion step
etsauer Feb 22, 2018
01542e6
Enable promotion step
etsauer Feb 22, 2018
7e84e15
Enable promotion step
etsauer Feb 22, 2018
c2de140
Enable promotion step
etsauer Feb 22, 2018
c3ea77f
Enable promotion step
etsauer Feb 22, 2018
0ad0700
Enable promotion step
etsauer Feb 22, 2018
3dbe609
Enable promotion step
etsauer Feb 22, 2018
c3ea68a
Enable promotion step
etsauer Feb 22, 2018
32425a0
Enable promotion step
etsauer Feb 22, 2018
7ad965d
Enable promotion step
etsauer Feb 22, 2018
cc538ee
Enable promotion step
etsauer Feb 22, 2018
08be3c9
README fix and jenkins memory limit update
etsauer Feb 22, 2018
053787e
Ading ansible-galaxy for dependency management
etsauer Feb 28, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions multi-cluster-spring-boot/Jenkinsfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
openshift.withCluster() {
env.NAMESPACE = openshift.project()
env.SKOPEO_IMAGE = openshift.selector( 'is/jenkins-slave-image-mgmt' ).object().status.dockerImageRepository
env.APP_NAME = "${env.JOB_NAME}".replaceAll(/-?pipeline-?/, '').replaceAll(/-?${env.NAMESPACE}-?/, '')
echo "Starting Pipeline for ${APP_NAME}..."
def projectBase = "${env.NAMESPACE}".replaceAll(/-dev/, '')
env.STAGE1 = "${projectBase}-dev"
env.STAGE2 = "${projectBase}-stage"
env.STAGE3 = "${projectBase}-prod"
}


pipeline {

agent { label 'maven' }

stages {

stage('Code Build') {
steps {
git url: "${SOURCE_CODE_URL}"
sh "mvn clean install -q"
}
}

stage('Image Build') {
steps {
echo 'Building Image from Jar File'
sh """
set +x
rm -rf oc-build && mkdir -p oc-build/deployments
for t in \$(echo "jar;war;ear" | tr ";" "\\n"); do
cp -rfv ./target/*.\$t oc-build/deployments/ 2> /dev/null || echo "No \$t files"
done
"""
script {
openshift.withCluster() {
openshift.startBuild("${APP_NAME}", "--from-dir=oc-build", "--wait", "--follow")
}
}
}
}

stage ('Verify Deployment to Dev') {
steps {
script {
openshift.withCluster() {
def dcObj = openshift.selector('dc', env.APP_NAME).object()
def podSelector = openshift.selector('pod', [deployment: "${APP_NAME}-${dcObj.status.latestVersion}"])
podSelector.untilEach {
echo "pod: ${it.name()}"
return it.object().status.containerStatuses[0].ready
}
}
}
}
}

stage('Promote to Stage') {
steps {
script {
openshift.withCluster() {
openshift.tag("${env.STAGE1}/${env.APP_NAME}:latest", "${env.STAGE2}/${env.APP_NAME}:latest")
}
}
}
}

stage ('Verify Deployment to Stage') {
steps {
script {
openshift.withCluster() {
openshift.withProject("${STAGE2}") {
def dcObj = openshift.selector('dc', env.APP_NAME).object()
def podSelector = openshift.selector('pod', [deployment: "${APP_NAME}-${dcObj.status.latestVersion}"])
podSelector.untilEach {
echo "pod: ${it.name()}"
return it.object().status.containerStatuses[0].ready
}
}
}
}
}
}

stage('Promote to Prod') {
agent {
kubernetes {
label 'promotion-slave'
cloud 'openshift'
serviceAccount 'jenkins'
containerTemplate {
name 'jnlp'
image "docker-registry.default.svc:5000/${NAMESPACE}/jenkins-slave-image-mgmt"
alwaysPullImage true
workingDir '/tmp'
args '${computer.jnlpmac} ${computer.name}'
ttyEnabled false
}
}
}
steps {
script {
openshift.withCluster() {

def localToken = readFile('/var/run/secrets/kubernetes.io/serviceaccount/token').trim()

def secretData = openshift.selector('secret/prod-credentials').object().data
def encodedRegistry = secretData.registry
def encodedToken = secretData.token
def registry = sh(script:"set +x; echo ${encodedRegistry} | base64 --decode", returnStdout: true)
def token = sh(script:"set +x; echo ${encodedToken} | base64 --decode", returnStdout: true)

openshift.withProject("${STAGE2}") {
def imageRegistry = openshift.selector( 'is', "${APP_NAME}").object().status.dockerImageRepository
echo "Promoting ${imageRegistry} -> ${registry}/${STAGE3}/${APP_NAME}"
sh """
set +x
skopeo copy --remove-signatures \
--src-creds openshift:${localToken} --src-cert-dir=/run/secrets/kubernetes.io/serviceaccount/ \
--dest-creds openshift:${token} --dest-tls-verify=false \
docker://${imageRegistry} docker://${registry}/${STAGE3}/${APP_NAME}
"""
}

}
}
}
}

stage ('Verify Deployment to Prod') {
steps {
script {
openshift.withCluster() {
def secretData = openshift.selector('secret/prod-credentials').object().data
def encodedAPI = secretData.api
def encodedToken = secretData.token
env.API = sh(script:"set +x; echo ${encodedAPI} | base64 --decode", returnStdout: true).replaceAll(/https?/, 'insecure')
env.TOKEN = sh(script:"set +x; echo ${encodedToken} | base64 --decode", returnStdout: true)
}
openshift.withCluster( env.API, env.TOKEN ) {
openshift.withProject("${STAGE3}") {
def dcObj = openshift.selector('dc', env.APP_NAME).object()
def podSelector = openshift.selector('pod', [deployment: "${APP_NAME}-${dcObj.status.latestVersion}"])
podSelector.untilEach {
echo "pod: ${it.name()}"
return it.object().status.containerStatuses[0].ready
}
}
}
}
}
}

}
}
176 changes: 176 additions & 0 deletions multi-cluster-spring-boot/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# A Sample OpenShift Pipeline for a Spring Boot Application

This example demonstrates how to implement a full end-to-end Jenkins Pipeline for a Java application in OpenShift Container Platform. This sample demonstrates the following capabilities:

* Deploying an integrated Jenkins server inside of OpenShift
* Running both custom and oob Jenkins slaves as pods in OpenShift
* "One Click" instantiation of a Jenkins Pipeline using OpenShift's Jenkins Pipeline Strategy feature
* Promotion of an application's container image within an OpenShift Cluster (using `oc tag`)
* Promotion of an application's container image to a separate OpenShift Cluster (using `skopeo`)
* Automated rollout using the [openshift-appler](https://github.com/redhat-cop/casl-ansible/tree/master/roles/openshift-applier) Ansible role.

## Prerequisites

In order to run this pipeline, you will need:

* Two (2) OpenShift clusters, version 3.5 or greater
* In this document, we will refer to the first cluster as *_Dev_* and the second as *_Prod_*.
* Ansible installed on your machine

## Automated Quickstart

This quickstart can be deployed quickly using Ansible. Here are the steps.

1. Clone [this repo](https://github.com/redhat-cop/container-pipelines.git)
2. `cd container-pipelines/multi-cluster-spring-boot`
3. Run `ansible-galaxy install -r requirements.yml --roles-path=roles`
4. Log into your _Prod_ OpenShift cluster, and run the following command.
```
$ oc login <prod cluster>
...
$ ansible-playbook -i ./applier/inventory-prod/ roles/casl-ansible/playbooks/openshift-cluster-seed.yml
```
5. One of the things that was created by ansible is a `ServiceAccount` that will be used for promoting your app from _Dev_ to _Prod_. We'll need to extract its credentials so that our pipeline can use that account.
```
TOKEN=$(oc serviceaccounts get-token promoter -n multicluster-spring-boot-prod)
```
The Ansible automation for your _Dev_ cluster will expect a parameters file to be created at `./applier/params/prod-credentials`. It should look something like this:
```
$ echo "TOKEN=${TOKEN}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to add more context on how to retrieve these values? Such as the registry?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sabre1041 until we have a consistent way to grab the registry URL, i think there's not point.

API_URL=https://master.example.com
REGISTRY_URL=docker-registry-default.apps.example.com
" > ./applier/params/prod-credentials
```
6. Now, Log into your _Dev_ cluster, and instantiate the pipeline.
```
$ oc login <dev cluster>
...
$ ansible-playbook -i ./applier/inventory-dev/ /path/to/casl-ansible/playbooks/openshift-cluster-seed.yml
```

At this point you should have 3 projects deployed (`multicluster-spring-boot-dev`, `multicluster-spring-boot-stage`, and `multicluster-spring-boot-prod`) with our [Spring Rest](https://github.com/redhat-cop/spring-rest.git) demo application deployed to all 3.

## Architecture

The following breaks down the architecture of the pipeline deployed, as well as walks through the manual deployment steps

### OpenShift Templates

The components of this pipeline are divided into two templates.

The first template, `applier/templates/build.yml` is what we are calling the "Build" template. It contains:

* A `jenkinsPipelineStrategy` BuildConfig
* An `s2i` BuildConfig
* An ImageStream for the s2i build config to push to

The build template contains a default source code repo for a java application compatible with this pipelines architecture (https://github.com/redhat-cop/spring-rest).

The second template, `applier/templates/deployment.yml` is the "Deploy" template. It contains:

* A tomcat8 DeploymentConfig
* A Service definition
* A Route

The idea behind the split between the templates is that I can deploy the build template only once (to my dev project) and that the pipeline will promote my image through all of the various stages of my application's lifecycle. The deployment template gets deployed once to each of the stages of the application lifecycle (once per OpenShift project).

### Pipeline Script

This project includes a sample `Jenkinsfile` pipeline script that could be included with a Java project in order to implement a basic CI/CD pipeline for that project, under the following assumptions:

* The project is built with Maven
* The `Jenkinsfile` script is placed in the same directory as the `pom.xml` file in the git source.
* The OpenShift projects that represent the Application's lifecycle stages are of the naming format: `<app-name>-dev`, `<app-name>-stage`, `<app-name>-prod`.

For convenience, this pipeline script is already included in the following git repository, based on our [Spring Boot Demo App](https://github.com/redhat-cop/spring-rest) app.

https://github.com/redhat-cop/spring-rest

## Manual Deployment Instructions

### 1. Create Lifecycle Stages

For the purposes of this demo, we are going to create three stages for our application to be promoted through.

- `multicluster-spring-boot-dev`
- `multicluster-spring-boot-stage`
- `multicluster-spring-boot-prod`

In the spirit of _Infrastructure as Code_ we have a YAML file that defines the `ProjectRequests` for us. This is as an alternative to running `oc new-project`, but will yeild the same result.

```
$ oc create -f applier/projects/projects.yml
projectrequest "multicluster-spring-boot-dev" created
projectrequest "multicluster-spring-boot-stage" created
projectrequest "multicluster-spring-boot-prod" created
```

### 2. Stand up Jenkins master in dev

For this step, the OpenShift default template set provides exactly what we need to get jenkins up and running.

```
$ oc process openshift//jenkins-ephemeral | oc apply -f- -n multicluster-spring-boot-dev
route "jenkins" created
deploymentconfig "jenkins" created
serviceaccount "jenkins" created
rolebinding "jenkins_edit" created
service "jenkins-jnlp" created
service "jenkins" created
```

### 4. Instantiate Pipeline

A _deploy template_ is provided at `applier/templates/deployment.yml` that defines all of the resources required to run our Tomcat application. It includes:

* A `Service`
* A `Route`
* An `ImageStream`
* A `DeploymentConfig`
* A `RoleBinding` to allow Jenkins to deploy in each namespace.

This template should be instantiated once in each of the namespaces that our app will be deployed to. For this purpose, we have created a param file to be fed to `oc process` to customize the template for each environment.

Deploy the deployment template to all three projects.
```
$ oc process -f applier/templates/deployment.yml --param-file=applier/params/deployment-dev | oc apply -f-
service "spring-rest" created
route "spring-rest" created
imagestream "spring-rest" created
deploymentconfig "spring-rest" created
rolebinding "jenkins_edit" configured
$ oc process -f applier/templates/deployment.yml --param-file=applier/params/deployments-stage | oc apply -f-
service "spring-rest" created
route "spring-rest" created
imagestream "spring-rest" created
deploymentconfig "spring-rest" created
rolebinding "jenkins_edit" created
$ oc process -f applier/templates/deployment.yml --param-file=applier/params/deployment-prod | oc apply -f-
service "spring-rest" created
route "spring-rest" created
imagestream "spring-rest" created
deploymentconfig "spring-rest" created
rolebinding "jenkins_edit" created
```

A _build template_ is provided at `applier/templates/build.yml` that defines all the resources required to build our java app. It includes:

* A `BuildConfig` that defines a `JenkinsPipelineStrategy` build, which will be used to define out pipeline.
* A `BuildConfig` that defines a `Source` build with `Binary` input. This will build our image.

Deploy the pipeline template in dev only.
```
$ oc process -f applier/templates/build.yml --param-file applier/params/build-dev | oc apply -f-
buildconfig "spring-rest-pipeline" created
buildconfig "spring-rest" created
```

At this point you should be able to go to the Web Console and follow the pipeline by clicking in your `multicluster-spring-boot-dev` project, and going to *Builds* -> *Pipelines*. At several points you will be prompted for input on the pipeline. You can interact with it by clicking on the _input required_ link, which takes you to Jenkins, where you can click the *Proceed* button. By the time you get through the end of the pipeline you should be able to visit the Route for your app deployed to the `myapp-prod` project to confirm that your image has been promoted through all stages.

## Cleanup

Cleaning up this example is as simple as deleting the projects we created at the beginning.

```
oc delete project multicluster-spring-boot-dev multicluster-spring-boot-prod multicluster-spring-boot-stage
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
openshift_cluster_content:
- object: projects
content:
- name: "create environments"
file: "{{ inventory_dir }}/../projects/projects.yml"
file_action: create
- object: deployments
content:
- name: "deploy jenkins"
template: "openshift//jenkins-ephemeral"
params: "{{ inventory_dir }}/../params/jenkins"
namespace: multicluster-spring-boot-dev
- name: "create prod cluster credential"
template: "{{ inventory_dir }}/../templates/cluster-secret.yml"
params: "{{ inventory_dir }}/../params/prod-credentials"
namespace: multicluster-spring-boot-dev
- name: "deploy dev environment"
template: "{{ inventory_dir }}/../templates/deployment.yml"
params: "{{ inventory_dir }}/../params/deployment-dev"
- name: "deply stage environment"
template: "{{ inventory_dir }}/../templates/deployment.yml"
params: "{{ inventory_dir }}/../params/deployment-stage"
- object: builds
content:
- name: Apply Image Build"
namespace: "multicluster-spring-boot-dev"
template: "https://raw.githubusercontent.com/redhat-cop/containers-quickstarts/master/jenkins-slaves/templates/jenkins-slave-image-mgmt-template.json"
params: "{{ inventory_dir }}/../params/build-slave-dev"
- name: "deploy build pipeline to dev"
template: "{{ inventory_dir }}/../templates/build.yml"
params: "{{ inventory_dir }}/../params/build-dev"
2 changes: 2 additions & 0 deletions multi-cluster-spring-boot/applier/inventory-dev/hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[seed-hosts]
localhost ansible_connection=local
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
openshift_cluster_content:
- object: projects
content:
- name: "create environments"
file: "{{ inventory_dir }}/../projects/projects-prod.yml"
file_action: create
- name: "create promoter account"
file: "{{ inventory_dir }}/../projects/promoter-sa.yml"
- object: deployments
content:
- name: "deply prod environment"
template: "{{ inventory_dir }}/../templates/deployment.yml"
params: "{{ inventory_dir }}/../params/deployment-prod"
2 changes: 2 additions & 0 deletions multi-cluster-spring-boot/applier/inventory-prod/hosts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[seed-hosts]
localhost ansible_connection=local
5 changes: 5 additions & 0 deletions multi-cluster-spring-boot/applier/params/build-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
APPLICATION_NAME=spring-rest
NAMESPACE=multicluster-spring-boot-dev
PIPELINE_REPOSITORY_URL=https://github.com/etsauer/container-pipelines.git
PIPELINE_REPOSITORY_REF=multi-cluster-promotion
PIPELINE_REPOSITORY_CONTEXT_DIR=multi-cluster-spring-boot
Empty file.
5 changes: 5 additions & 0 deletions multi-cluster-spring-boot/applier/params/deployment-dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
APPLICATION_NAME=spring-rest
NAMESPACE=multicluster-spring-boot-dev
SA_NAMESPACE=multicluster-spring-boot-dev
READINESS_RESPONSE=status.:.UP
READINESS_PATH=/health
Loading