diff --git a/exercises/1-the-manual-menace/README.md b/exercises/1-the-manual-menace/README.md index 621ac39b..62050026 100644 --- a/exercises/1-the-manual-menace/README.md +++ b/exercises/1-the-manual-menace/README.md @@ -149,17 +149,42 @@ https:///f?url=https://raw.githubusercontent.com/rht-labs/enable 3. Open the `inventory/groups_vars/all.yml` file. Update the `namespace_prefix` variables by replacing the `` (including the `<` and `>`) with your name or initials. **Don't use uppercase or special characters**. For example; if your name is Tim Smith you would replace `` and set `namespace_prefix` to something like `tim` or `tsmith`. -📝 _enablement-ci-cd/inventory/groups_vars/all.yml_ +📝 enablement-ci-cd/inventory/groups_vars/all.yml + + + +#### ** Important Part ** ```yaml namespace_prefix: "" ``` +#### ** Entire File ** + +```yaml +--- +# Please change '' below to be unique for your deployment +# Note: +# - keep it lowercase +# - do NOT use special characters +# - make sure to replace the entire string between the double quotes - including the '<' and '>' + +namespace_prefix: "" # ⬅️ We care about this part! + +openshift_templates_raw: "https://raw.githubusercontent.com/rht-labs/openshift-templates" +openshift_templates_raw_version_tag: "v1.4.17" +cop_quickstarts: "https://github.com/redhat-cop/containers-quickstarts.git" +cop_quickstarts_raw: "https://raw.githubusercontent.com/redhat-cop/containers-quickstarts" +cop_quickstarts_raw_version_tag: "v1.29" +``` + + + 4. Open the `inventory/host_vars/projects-and-policies.yml` file; you should see some variables setup already to create the `-ci-cd` namespace. This object is passed to the OpenShift Applier to call the `templates/project-requests.yml` template with the parameters composed from the inventory and the `ci_cd` vars in the `apply.yml` playbook. We will add some additional content here but first let's explore the parameters and the template 5. Inside of the `inventory/host_vars/projects-and-policies.yml` you'll see the following -📝 _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +👀 enablement-ci-cd/inventory/host_vars/projects-and-policies.yml ```yaml ci_cd: @@ -169,7 +194,7 @@ ci_cd: - This will define the variables that we'll soon be using to deploy our CI/CD project. It relies on the `namespace_prefix` that we updated earlier. Pulling these two sets of variables together will now allow us to pass the newly created variables to our template that will create our project appropriately. You'll notice that the name of the variable above (`ci_cd`) is then assigned to `params_from_vars` in our inventory. -📝 _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +👀 enablement-ci-cd/inventory/host_vars/projects-and-policies.yml ```yaml ansible_connection: local @@ -188,7 +213,11 @@ openshift_cluster_content: - In your editor, open `enablement-ci-cd/inventory/host_vars/projects-and-policies.yml` and add the following lines before `openshift_cluster_content`: -📝 _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +📝 enablement-ci-cd/inventory/host_vars/projects-and-policies.yml + + + +#### ** Important Part ** ```yaml dev: @@ -200,9 +229,43 @@ test: NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + NAMESPACE: "{{ namespace_prefix }}-ci-cd" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title}}s CI/CD" + +dev: + NAMESPACE: "{{ namespace_prefix }}-dev" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Dev" + +test: + NAMESPACE: "{{ namespace_prefix }}-test" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" + +ansible_connection: local +openshift_cluster_content: +- object: projectrequest + content: + - name: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ ci_cd }}" + tags: + - projects +``` + + + 7. In the `enablement-ci-cd/inventory/host_vars/projects-and-policies.yml` file, add the new objects for the projects you want to create (dev & test) by adding another object to the `content` array (previously defined) for each. You can copy and paste them from the `ci_cd_namespace` example and update them accordingly. If you do this, remember to set the names to `{{ dev_namespace }}` and `{{ test_namespace }}` and change the `params_from_vars` variable accordingly. The values for these variables used for the names (`ci_cd_namespace`, `dev_namespace` etc.) are defined in `apply.yml` file in the root of the project. -📝 _enablement-ci-cd/inventory/host_vars/projects-and-policies.yml_ +📝 enablement-ci-cd/inventory/host_vars/projects-and-policies.yml + + + +#### ** Important Part ** ```yaml - name: "{{ dev_namespace }}" @@ -219,6 +282,48 @@ test: - projects ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + NAMESPACE: "{{ namespace_prefix }}-ci-cd" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title}}s CI/CD" + +dev: + NAMESPACE: "{{ namespace_prefix }}-dev" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Dev" + +test: + NAMESPACE: "{{ namespace_prefix }}-test" + NAMESPACE_DISPLAY_NAME: "{{ namespace_prefix | title }} Test" + +ansible_connection: local +openshift_cluster_content: +- object: projectrequest + content: + - name: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ ci_cd }}" + tags: + - projects + - name: "{{ dev_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ dev }}" + tags: + - projects + - name: "{{ test_namespace }}" + template: "{{ playbook_dir }}/templates/project-requests.yml" + action: create + params_from_vars: "{{ test }}" + tags: + - projects +``` + + + 8. Use the `Terminal > Open Terminal in specific container` menu item to open a terminal in the `node-rhel7-ansible` container ![open-terminal](../images/exercise1/open-terminal.png) @@ -291,18 +396,35 @@ touch params/nexus 2. The essential params to include in this file are: -📝 _enablement-ci-cd/params/nexus_ +📝 enablement-ci-cd/params/nexus + + + +#### ** Important Part ** + +``` +VOLUME_CAPACITY=5Gi +MEMORY_LIMIT=1Gi +``` + +#### ** Entire File ** ``` VOLUME_CAPACITY=5Gi MEMORY_LIMIT=1Gi ``` + + - You'll notice that this is different from how we defined our params for our projects. This is because there are multiple ways to do this. In cases like this, there may be a need to change some of these variables more frequently than others (i.e. giving the app more memory,etc.). In this case, it's easier to maintain them within their own separate params files. 3. Create a new object in the inventory variables `inventory/host_vars/ci-cd-tooling.yml` called `ci-cd-tooling` and populate its `content` as follows -📝 _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +📝 enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml --- @@ -320,6 +442,26 @@ openshift_cluster_content: - nexus ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local +openshift_cluster_content: + - galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" + - object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus +``` + + + ![ci-cd-deployments-yml](../images/exercise1/ci-cd-deployments-yml.png)

@@ -372,7 +514,11 @@ git push -u origin --all 1. Open `enablement-ci-cd` in your favourite editor. Edit the `inventory/host_vars/ci-cd-tooling.yml` to include a new object for our mongodb as shown below. This item can be added below Nexus in the `ci-cd-tooling` section. -📝 _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +📝 enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml - name: "jenkins-mongodb" @@ -383,6 +529,33 @@ git push -u origin --all - mongodb ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb +``` + + + ![jenkins-mongo](../images/exercise1/jenkins-mongo.png) 2. Git commit your updates to the inventory to git for traceability. @@ -423,7 +596,11 @@ ansible-playbook apply.yml -e target=tools \ touch params/jenkins ``` -📝 _enablement-ci-cd/params/jenkins_ +📝 enablement-ci-cd/params/jenkins + + + +#### ** Important Part ** ``` MEMORY_LIMIT=3Gi @@ -433,6 +610,18 @@ NAMESPACE=-ci-cd JENKINS_OPTS=--sessionTimeout=720 ``` +#### ** Entire File ** + +``` +MEMORY_LIMIT=3Gi +VOLUME_CAPACITY=15Gi +JVM_ARCH=x86_64 +NAMESPACE=-ci-cd +JENKINS_OPTS=--sessionTimeout=720 +``` + + + - You might be wondering why we have to replace here and can't just rely on the `namespace_prefix` variable that we've been using previously. This is because the replacement is handled by two different engines (one being ansible -- which knows about `namespace_prefix` and the other being the oc client, which does not). Because the params files are processed by the oc client, we need to update this here. 2. Add a `jenkins` variable to the Ansible inventory underneath the jenkins-mongo in `inventory/host_vars/ci-cd-tooling.yml` as shown below to create a DeploymentConfig for Jenkins. In order for Jenkins to be able to run `npm` commands we must configure a jenkins build agent for it to use. This agent will be dynamically provisioned when we run a build. It needs to have Node.js and npm and a C compiler installed in it. @@ -441,7 +630,11 @@ JENKINS_OPTS=--sessionTimeout=720 NOTE These agents can take a time to build themselves so to speed up we have placed the agent with a corresponding ImageStream within OpenShift. To leverage this existing agent image, we are using a feature of the openshift-applier to process a couple of post-steps part of the inventory. These steps are utilized to perform pre and post tasks necessary to make our inventory work correctly. In this case, we use the post steps to tag and label the jenkins-agent-npm ImageStream within our CI/CD project so Jenkins knows how to find and use it.

-📝 _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +📝 enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml - name: "jenkins" @@ -462,6 +655,49 @@ JENKINS_OPTS=--sessionTimeout=720 - jenkins ``` +#### ** Entire File ** + +```yaml +--- +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +``` + + + This configuration, if applied now, will create the deployment configuration needed for Jenkins but the `${NAMESPACE}:${JENKINS_IMAGE_STREAM_TAG}` in the template won't exist yet. 3. To create this image we will take the supported OpenShift Container Platform Jenkins Image and bake in some extra configuration using an [s2i](https://github.com/openshift/source-to-image) builder image. More information on Jenkins s2i is found on the [openshift/jenkins](https://github.com/openshift/jenkins#installing-using-s2i-build) GitHub page. To create an s2i configuration for Jenkins, start with the pre-canned configuration source in the `enablement-ci-cd` repo (in the jenkins-s2i directory). @@ -498,7 +734,21 @@ slack:2.37 touch params/jenkins-s2i ``` -📝 _enablement-ci-cd/params/jenkins-s2i_ +📝 enablement-ci-cd/params/jenkins-s2i + + + +#### ** Important Part ** + +``` +SOURCE_REPOSITORY_URL= +NAME=jenkins +SOURCE_REPOSITORY_CONTEXT_DIR=jenkins-s2i +SOURCE_REPOSITORY_PASSWORD= +SOURCE_REPOSITORY_USERNAME= +``` + +#### ** Entire File ** ``` SOURCE_REPOSITORY_URL= @@ -508,26 +758,83 @@ SOURCE_REPOSITORY_PASSWORD= SOURCE_REPOSITORY_USERNAME= ``` + + where _ `` is the full clone path of the repo where this project is stored (including the https && .git) _ `` is the username builder pod will use to login and clone the repo with \* `` is the password the builder pod will use to authenticate and clone the repo using 6. At the top of `inventory/host_vars/ci-cd-tooling.yml` file underneath the `---`, add the following: -📝 _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ +📝 enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + +```yaml +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" +``` + +#### ** Entire File ** ```yaml +--- ci_cd: IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins ``` + + 7. Create a new object `ci-cd-builds` in the Ansible `inventory/host_vars/ci-cd-tooling.yml` to drive the s2i build configuration.

NOTE ⚡ - We are using a custom jenkins template that works with latest version of OpenShift until the changes can be merged upstream.

-📝 _enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml_ + +📝 enablement-ci-cd/inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** ```yaml - object: ci-cd-builds @@ -541,6 +848,61 @@ ci_cd: - jenkins ``` +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +``` + + + 8. Commit your code to your GitLab instance ```bash diff --git a/exercises/2-attack-of-the-pipelines/README.md b/exercises/2-attack-of-the-pipelines/README.md index 18d5b0e3..f8ea4366 100644 --- a/exercises/2-attack-of-the-pipelines/README.md +++ b/exercises/2-attack-of-the-pipelines/README.md @@ -135,7 +135,7 @@ Make sure that you are in your `-codeready` project while running this This updates the API endpoint in the `index.js` config file. Before you run the command, it will look like the following. -📝 *todolist/src/config/index.js* +👀 todolist/src/config/index.js ``` export default { todoEndpoint: "/api/todos" @@ -146,7 +146,9 @@ Afterwards, you should see something like this: ![fixApiUrl](../images/exercise2/black-magic.png) 8. The `todolist` has some scripts defined in the package.json at the root of the project. A snippet of the npm scripts are shown below. To run any of these scripts run `npm run `. -📝 *todolist/package.json* + +👀 todolist/package.json + ``` "scripts": { "serve": "vue-cli-service serve --open", @@ -345,15 +347,39 @@ with the following 2. Before we do this we need to change `` accordingly in the apply.yml file. -📝 *todolist/.openshift-applier/apply.yml* +📝 todolist/.openshift-applier/apply.yml + + + +#### ** Important Part ** + +```yaml +- name: Build and Deploy todolist + hosts: app + vars: + namespace_prefix: '' + ci_cd_namespace: '{{ namespace_prefix }}-ci-cd' ``` + +#### ** Entire File ** + +```yaml +--- - name: Build and Deploy todolist hosts: app vars: namespace_prefix: '' ci_cd_namespace: '{{ namespace_prefix }}-ci-cd' + dev_namespace: '{{ namespace_prefix }}-dev' + test_namespace: '{{ namespace_prefix }}-test' + tasks: + - include_role: + name: openshift-applier/roles/openshift-applier + ``` + + ![applier](../images/exercise2/applier.png) 3. With those changes in place we can now run the playbook. First install the `openshift-applier` dependency, using the `ansible-galaxy tool` as per exercise one and then run the playbook (from the todolist directory). This will populate the cluster with all the config needed for the front end app. @@ -561,7 +587,12 @@ Some of the key things to note: 2. The Jenkinsfile is mostly complete, however some minor changes will be needed to orchestrate namespaces. Find and replace all instances of `` in the Jenkinsfile. Update the `` to the one you log in to the cluster with; this variable is used in the namespace of your Git projects when checking out code etc. Replace `` with your Git domain (only the hostname, without `https://` or the repository name). -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy // Jenkinsfile @@ -582,6 +613,197 @@ Some of the key things to note: } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + // sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} + +``` + + + 3. With these changes in place, push your changes to the `develop` branch. ```bash git add Jenkinsfile diff --git a/exercises/3-revenge-of-the-automated-testing/README.md b/exercises/3-revenge-of-the-automated-testing/README.md index ce609a0f..6f781d13 100644 --- a/exercises/3-revenge-of-the-automated-testing/README.md +++ b/exercises/3-revenge-of-the-automated-testing/README.md @@ -8,7 +8,7 @@ ## Introduction to TDD. -**Test Driven Development (TDD)** is a software development process that relies on the repetition of a very short development cycle. Requirements are turned into test cases, where the software is developed to pass the tests. In other words, it creates a safety net that serves to keep the developer's problems/bugs at bay while enabling the developer to refactor efficiently. This is opposed to software development that allows software to be added that is not proven to meet requirements. +**Test Driven Development (TDD)** is a software development process that relies on the repetition of a very short development cycle. Requirements are turned into test cases, and the software is developed to pass the tests. In other words, it creates a safety net that serves to keep the developer's problems/bugs at bay while enabling the developer to refactor efficiently. This is opposed to software development that allows software to be added that is not proven to meet requirements. The TDD cycle can be illustrated with the following diagram: @@ -17,30 +17,31 @@ The TDD cycle can be illustrated with the following diagram: ### The TDD Cycle 1. `Write a test` - -In TDD a new feature begins by writing a test. Write a test that clearly defines a function or one that provides an improvement to an existing function. It's important the developer clearly understands the feature's specification and requirements, or the feature could be wrong from the get-go. + In TDD a new feature begins by writing a test. Write a test that clearly defines a function or one that provides an improvement to an existing function. It's important the developer clearly understands the feature's specification and requirements, or the feature could be wrong from the get-go. -2. `Test Fails` - -When a test is first implemented it is expected to fail. This failure validates the test is working correctly as the feature is yet to be implemented. +2. `The test fails` - + When a test is first implemented it is expected to fail. This failure validates the test is working correctly as the feature is yet to be implemented. -3. `Write code to make test pass` - -This step involves implementing the feature to pass the failed test. Code written at this stage may be inelegant and still pass the test, however this is acceptable as TDD is a recursive cycle which includes code refactoring. +3. `Write code to make the test pass` - + This step involves implementing the feature to pass the failed test. Code written at this stage may be inelegant and still pass the test, however this is acceptable as TDD is a recursive cycle which includes code refactoring. -4. `Code Passes tests` - -If all tests pass, the developer can be confident that the new code meets the test requirements. +4. `The test passes` - + If all tests pass, the developer can be confident that the new code meets the test requirements. 5. `Refactor` - -The refactoring step will allow the developer to clean up their code without changing its behaviour. Not changing the behaviour should ensure the tests still pass. The process of refactoring can include; removal of duplication, renaming of object, class, module, variable and method names to clearly represent their current purpose and use, decoupling of functionality and increasing code cohesion. + The refactoring step will allow the developer to clean up their code without changing its behaviour. Not changing the behaviour should ensure the tests still pass. The process of refactoring can include; removal of duplication, renaming of object, class, module, variable and method names to clearly represent their current purpose and use, decoupling of functionality and increasing code cohesion. 6. `Repeat` - -Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert in preference to excessive debugging. + Starting with another new test, the cycle is then repeated to push forward the functionality. The size of the steps should always be small, with as few as 1 to 10 edits between each test run. If new code does not rapidly satisfy a new test, or other tests fail unexpectedly, the programmer should undo or revert rather than excessively debuggin. ### Testing Bananalogy -Explanation of Mocha and JS test syntax through Bananalogy! Imagine for a moment; we're not building software but creating a bowl of fruit. To create a `Bunch of Bananas` component for our fruit bowl we could start with our tests as shown below. + +Explanation of Mocha and JS test syntax through a Bananalogy! Imagine for a moment that we're not building software but creating a bowl of fruit. To create a `Bunch of Bananas` component for our fruit bowl we could start with our tests as shown below. ![bdd-bananas](../images/exercise3/bdd-bananas.png) - * `describe` is used to group tests together. The string `"a bunch of ripe bananas"` is for human reading and allows you to identify tests. - * `it` is a statement that contains a test. It should contain an assertion such as `expect` or `should`. It follows the syntax of `describe` where the string passed in identifies the statement. +- `describe` is used to group tests together. The string `"a bunch of ripe bananas"` is for human reading and allows you to identify tests. +- `it` is a statement that contains a test. It should contain an assertion such as `expect` or `should`. It follows the syntax of `describe` where the string passed in identifies the statement. --- @@ -48,21 +49,22 @@ Explanation of Mocha and JS test syntax through Bananalogy! Imagine for a moment As a learner you will be able to -* Understand the why behind TDD -* Implement a feature using TDD for front end and backend -* Write end to end tests for the feature and run them in CI +- Understand the why behind TDD +- Implement a feature using TDD for front end and backend +- Write end-to-end tests for a feature and run them in the CI pipeline ## Tools and Frameworks 1. [Jest](https://facebook.github.io/jest/) - Zero configuration testing platform -Jest is used by Facebook to test all JavaScript code including React applications. One of Jest's philosophies is to provide an integrated "zero-configuration" experience. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in more stable and healthy code bases. + Jest is used by Facebook to test all JavaScript code including React applications. One of Jest's philosophies is to provide an integrated "zero-configuration" experience. We observed that when engineers are provided with ready-to-use tools, they end up writing more tests, which in turn results in more stable and healthy code bases. 1. [Vue Test Utils](https://vue-test-utils.vuejs.org/en/) - Vue Test Utils is the official unit testing utility library for Vue.js. 1. [Nightwatch.js](http://nightwatchjs.org/) - Nightwatch.js is an easy to use Node.js based End-to-End (E2E) testing solution for browser based apps and websites. It uses the powerful W3C WebDriver API to perform commands and assertions on DOM elements. -1. [Mocha](https://mochajs.org/) - Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. Hosted on GitHub. +1. [Mocha](https://mochajs.org/) - Mocha is a feature-rich JavaScript test framework running on Node.js and in the browser, making asynchronous testing simple and fun. Mocha tests run serially, allowing for flexible and accurate reporting, while mapping uncaught exceptions to the correct test cases. 1. [Sinon](http://sinonjs.org/) - Standalone test spies, stubs and mocks for JavaScript. -Works with any unit testing framework. + Works with any unit testing framework. ## Big Picture + > From the previous exercise; we created a simple pipeline. We will now flesh it out with some testing to add gates to our pathway to production. ![big-picture](../images/big-picture/big-picture-3.jpg) @@ -88,12 +90,15 @@ _On page load:_ ## Step by Step Instructions ### Part 1 - Tests in our Pipeline -> _In this part we will get familiar with the layout of our tests. We will also improve the pipeline created already by adding some unit tests for the front end & backend along with some end to end tests (e2e) to validate the full solution_ + +> _In this part we will get familiar with the layout of our tests. We will also improve the pipeline created already by adding some unit tests for the frontend and backend along with some end-to-end tests (e2e) to validate the full solution_ #### 1a - Unit tests + > In this exercise we will execute our test for the front end locally. Once verified we will add them to Jenkins. -1. Before linking our automated testing to the pipeline we'll first ensure the tests run locally. Change to the `todolist` directory and run `test` on the `develop` branch. +1. Before linking our automated testing to the pipeline, we'll first ensure the tests run locally. Change to the `todolist` directory and run `test` on the `develop` branch. + ```bash cd /projects/todolist ``` @@ -105,6 +110,7 @@ git checkout develop ```bash npm run test:client ``` +

NOTE - test:client is an alias used that runs vue-cli-service test from the scripts object in package.json

@@ -115,18 +121,22 @@ npm run test:client ![test-run-locally](../images/exercise3/test-run-locally.png) -4. Let's now try and get our Server tests running locally. Ensure your Database is running by opening a new terminal session and run the following command. This will start our mongodb locally. +4. Let's now try and get our Server tests running locally. Ensure your Database is running by opening a new terminal session and run the following command. This will start our mongodb locally. + ```bash npm run mongo:start-ide ``` +

NOTE - you can skip this step if you have your DB running from previous exercise

-5. Run your server side test with the following command. This will fire some requests against the API, validating that CRUD is working. +5. Run your server side test with the following command. This will fire some requests against the API, validating that CRUD is working. + ```bash npm run test:server ``` + ![test-server-run-locally](../images/exercise3/test-server-run-locally.png)

@@ -135,8 +145,13 @@ npm run test:server 6. With our tests all passing in the cloud ide, let's add them to our pipeline. Open the `Jenkinsfile` in your editor and add the command to run all the tests in the `steps{}` part of the `node-build` stage. -📝 todolist/Jenkinsfile -```Jenksfile +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy steps { sh 'printenv' @@ -147,10 +162,205 @@ steps { sh 'npm run test:all:ci' ``` -7. Running the tests is important, but so is reporting the results. In the `post{}` `always{}` section of the `Jenkinsfile` add the location for Jenkins for find the test reports +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} +``` + + + +7. Running the tests is important, but so is reporting the results. In the `post{}` `always{}` section of the `Jenkinsfile`, add the location for Jenkins to find the test reports -📝 todolist/Jenkinsfile -```Jenksfile +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy post { always { archive "**" @@ -158,9 +368,200 @@ post { junit 'test-report.xml' junit 'reports/server/mocha/test-results.xml' } +} +``` + +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + } +} ``` -8. With this in place, commit the changes which should trigger a build. + + +8. With this in place, commit the changes, which should trigger a build. + ```bash git add Jenkinsfile git commit -m "Adding unit tests to the pipeline" @@ -169,18 +570,20 @@ git push 9. Navigate to your instance of Jenkins at `https://jenkins--ci-cd.` and you should see the tests are now running in your pipeline. You should see a test trend graph only after two runs of the build - #### 1b - End to End Tests (e2e) + > _Unit tests are a great way to get immediate feedback as part of testing an application. End to end tests that drive user behaviour are another amazing way to ensure an application is behaving as expected._ In this part of the exercise, we will add a new stage to our pipeline called `todolist-e2e` that will run after the deploy has been completed. End to end tests will use `Nightwatch.js` to orchestrate a Selenium WebDriver instance that controls the web browser; in this case Google Chrome! 1. Ensure the `todolist` app is running in a separate terminal shell. + ``` npm run serve:all ``` 2. Let's start by checking that our tests execute in the cloud ide. Our end to end tests are stored in `tests/e2e/specs/`. The VueJS cli uses `Nightwatch.js` and comes pre-configured to run tests against Google Chrome. The tests run headlessly in our CodeReady workspace. To get them executing, open a new Terminal and fire up the Selenium service and leave it running. + ```bash cd /projects/todolist npm run selenium @@ -191,6 +594,7 @@ npm run selenium

3. On a new terminal move to the `todolist` folder. Run the tests locally by executing the following command. This should start the dev server and run the test. + ```bash cd /projects/todolist npm run e2e:ide @@ -200,55 +604,473 @@ npm run e2e:ide 4. With tests executing successfully locally; let's add them to our Jenkins pipeline. To do this; we'll create a new stage in our `Jenkinsfile`. Create a new `stage` called `e2e test` to run after the `node-deploy stage` -📝 todolist/Jenkinsfile -```Jenksfile +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy stage("e2e test") { - + } ``` -![e2e-stage-new](../images/exercise3/e2e-stage-new.png) -5. Set the agent that this stage should execute on. In this case it will use the same `jenkins-agent-npm` that was used in the build stage. Set the steps needed to execute the tests and add the reporting location +#### ** Entire File ** -📝 todolist/Jenkinsfile -```Jenksfile -stage("e2e test") { +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { agent { node { - label "jenkins-agent-npm" + label "master" } } when { - expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + expression { GIT_BRANCH ==~ /(.*master)/ } } steps { - unstash 'source' + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' - echo '### Install deps ###' - sh 'npm install' + echo '### Install deps ###' + sh 'npm install' - echo '### Running end to end tests ###' - sh 'npm run e2e:jenkins' + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' } + // Post can be used both on individual stages and for the entire build. post { always { - junit 'reports/e2e/specs/*.xml' + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" } } } -``` -6. With this in place, commit the changes to trigger a build and enhance our pipeline -```bash -git add Jenkinsfile -git commit -m "Adding e2e tests to the pipeline" -git push -``` + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } -7. Jenkins should now show the additional stage in the pipeline view for the branch -![e2e-pipeline](../images/exercise3/e2e-pipeline.png) + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + + } + } +} +``` + + + +![e2e-stage-new](../images/exercise3/e2e-stage-new.png) + +5. Set the agent that this stage should execute on. In this case it will use the same `jenkins-agent-npm` that was used in the build stage. Set the steps needed to execute the tests and add the reporting location + +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + +```groovy +stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } +``` + +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + } +} +``` + + + +6. With this in place, commit the changes to trigger a build and enhance our pipeline + +```bash +git add Jenkinsfile +git commit -m "Adding e2e tests to the pipeline" +git push +``` + +7. Jenkins should now show the additional stage in the pipeline view for the branch + ![e2e-pipeline](../images/exercise3/e2e-pipeline.png) + +8. After confirming the pipeline is successful on the `develop` branch, let's bring these changes back into the main branch. -8. After confirming the pipeline is successful on the `develop` branch, let's bring these changes back into the main branch. ```bash git checkout master ``` @@ -265,23 +1087,28 @@ git commit -m "Updated Jenkinsfile taken from develop branch" git push ``` - ### Part 2 - TodoList new feature -> _In this exercise we will introduce a new feature to create an important flag on the todos. In order to be able to build and test our feature we will use TDD_ -*As a doer I want to mark todos as important so that I can keep track of and complete high priority todos first* +> _In this exercise we will introduce a new feature to create an important flag on the todos. We will use Test Driven Development to build and test our feature_ + +Let's look at a user story for the new feature we want to add: + +_As a todolist user, I want to mark todos as important so that I can keep track of and complete high priority todos first_ _Acceptance Criteria_ + - [ ] should be doable with a single click - [ ] should add a red flag against the todo when marked important - [ ] should remove the red colour flag on the flag when important removed - [ ] should not affect existing todos _On page load:_ + - [ ] should display existing todos that are not marked important - [ ] should display existing todos that are marked important with an red flag #### 2a - Create todolist api tests + > Using [Mocha](https://mochajs.org/) as our test runner; we will now write some tests for backend functionality to persist our important-flag. The changes required to the backend are minimal but we will use TDD to create our test first, then implement the functionality. 1. Create a new branch in your `todolist` app for our feature and push it to the remote @@ -289,183 +1116,775 @@ _On page load:_ ```bash cd todolist ``` + ```bash git checkout -b feature/important-flag ``` + ```bash git push -u origin feature/important-flag ``` 2. Navigate to the `server/api/todo/todo.spec.js` file. This contains all of the existing todo list api tests. These are broken down into simple `describe("api definition", function(){})` blocks which is BDD speak for how the component being tested should behave. Inside of each `it("should do something ", function(){})` statements we use some snappy language to illustrate the expected behaviour of the test. For example a `GET` request of the api is described and tested for the return to be of type Array as follows. -📝 todolist/server/api/todo/todo.spec.js +👀 todolist/server/api/todo/todo.spec.js📝 todolist/server/api/todo/todo.spec.js + + + +#### ** Important Part ** -📝 todolist/server/api/todo/todo.spec.js ```javascript // Exercise 3 test case! -it("should mark todo as important and persist it", function(done) { +it("should mark todo as important and persist it", function (done) { + request(app) + .put("/api/todos/" + todoId) + .send({ + title: "LOVE endpoint/server side testing!", + completed: true, + important: true, + }) + .expect(200) + .expect("Content-Type", /json/) + .end(function (err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + // YOUR TEST GO HERE + res.body.important.should.equal(true); + done(); + }); +}); +``` + +#### ** Entire File ** + +```javascript +"use strict"; + +const app = require("../../app"); +const request = require("supertest"); +require("should"); + +describe("GET /api/todos", function() { + it("should respond with JSON array", function(done) { + request(app) + .get("/api/todos") + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.be.instanceof(Array); + done(); + }); + }); +}); + +describe("POST /api/todos", function() { + it("should create the todo and return with the todo", function(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("learn about endpoint/server side testing"); + res.body.completed.should.equal(false); + done(); + }); + }); +}); + +describe("GET /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should update the todo", function(done) { + request(app) + .get("/api/todos/" + todoId) + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body._id.should.equal(todoId); + res.body.title.should.equal("learn about endpoint/server side testing"); + res.body.completed.should.equal(false); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .get("/api/todos/" + "abcdef0123456789ABCDEF01") + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .get("/api/todos/" + 123) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); +}); + +describe("DELETE /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should delete the todo", function(done) { + request(app) + .delete("/api/todos/" + todoId) + .expect(204) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .delete("/api/todos/" + "abcdef0123456789ABCDEF01") + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .delete("/api/todos/" + 123) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); +}); + +describe("PUT /api/todos/:id", function() { + let todoId; + beforeEach(function createObjectToUpdate(done) { + request(app) + .post("/api/todos") + .send({ + title: "learn about endpoint/server side testing", + completed: false + }) + .expect(201) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + todoId = res.body._id; + done(); + }); + }); + it("should update the todo", function(done) { + request(app) + .put("/api/todos/" + todoId) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(200) + .expect("Content-Type", /json/) + .end(function(err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + res.body.completed.should.equal(true); + done(); + }); + }); + it("should return 404 for valid mongo object id that does not exist", function(done) { + request(app) + .put("/api/todos/" + "abcdef0123456789ABCDEF01") + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(404) + .end(function(err) { + if (err) return done(err); + done(); + }); + }); + it("should return 400 for invalid object ids", function(done) { + request(app) + .put("/api/todos/" + 123) + .send({ title: "LOVE endpoint/server side testing!", completed: true }) + .expect(400) + .end(function(err, res) { + if (err) return done(err); + res.text.should.equal("not a valid mongo object id"); + done(); + }); + }); + + + + // Exercise 3 test case! + it("should mark todo as important and persist it", function (done) { request(app) .put("/api/todos/" + todoId) .send({ title: "LOVE endpoint/server side testing!", completed: true, - important: true + important: true, }) .expect(200) .expect("Content-Type", /json/) - .end(function(err, res) { - if (err) return done(err); - res.body.should.have.property("_id"); - res.body.title.should.equal("LOVE endpoint/server side testing!"); - // YOUR TEST GO HERE - res.body.important.should.equal(true); - done(); + .end(function (err, res) { + if (err) return done(err); + res.body.should.have.property("_id"); + res.body.title.should.equal("LOVE endpoint/server side testing!"); + // YOUR TEST GO HERE + res.body.important.should.equal(true); + done(); }); + }); + }); ``` + + 6. Run your test. It should fail. + ```bash npm run test:server ``` ![fail-mocha](../images/exercise3/fail-mocha.png) -7. With our test now failing; let's implement the feature. This is quite a simple change - we first need to update the `server/api/todo/todo.model.js`. Add an additional property on the schema called `important` and make its type Boolean. +7. With our test now failing, let's implement the feature. This is quite a simple change—we first need to update the `server/api/todo/todo.model.js`. + Add an additional property on the schema called `important` and make its type `Boolean`. + +📝 todolist/server/api/todo/todo.model.js + + + +#### ** Important Part ** -📝 todolist/server/api/todo/todo.model.js ```javascript const TodoSchema = new Schema({ title: String, - completed: Boolean, - important: Boolean + completed: Boolean, + important: Boolean, }); ``` +#### ** Entire File ** + +```javascript +'use strict'; + +const mongoose = require('mongoose'), + Schema = mongoose.Schema; + +const TodoSchema = new Schema({ + title: String, + completed: Boolean, + important: Boolean, +}); + +module.exports = mongoose.model('Todo', TodoSchema); +``` + + + 8. Next we need to update the `server/config/seed.js` file so that the pre-generated todos have an important property. Add `important: false` below `completed: *` for each object. Don't forget to add a comma at the end of the `completed: *` line. ![api-add-seed-important](../images/exercise3/api-add-seed-important.png) 9. With your changes to the Database schema updated; re-run your tests. The tests should pass. + ```bash npm run test:server ``` -10. Commit your code to the `feature/important-flag` branch and then merge onto the `develop` branch as follows +10. Commit your code to the `feature/important-flag` branch and then merge onto the `develop` branch as follows ```bash git add . ``` + ```bash git commit -m "ADD backend schema updates" ``` + ```bash git push ``` + ```bash git checkout develop ``` + ```bash git merge feature/important-flag ``` -When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. +When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. + +```bash +git push +``` + +#### 2b - Create todolist front-end tests + +> Using [Jest](https://facebook.github.io/jest/) as our test runner and the `vue-test-utils` library for managing our vue components; we will now write some tests for front end functionality to persist our important-flag. The changes required to the front end are quite large but we will use TDD to create our test first, then implement the functionality. + +Our TodoList App uses `vuex` to manage the state of the app's todos and `axios` HTTP library to connect to the backend. +`Vuex` is an opinionated framework for managing application state and has some key design features you will need to know to continue with the exercise. + +In `vuex` the application state is managed by a `store`. The `store` houses all the todos we have retrieved from the backend +as well as the `getter` methods for our array of `todos`. In order to make changes to the store, we could call the store directly +and update each todo item but as earlier said; vuex is an opinionated module with its own way of updating the store. +It is bad practice to call the store directly. + +There are two parts of the lifecycle to updating the store, the `actions` & `mutations`. When the user clicks a todo to mark it as complete, +the `actions` are called. An action could involve a call to the backend or some pre-processing of the data. Once this is done, +the change is committed to the store by calling the `mutation` function. A store should only ever be manipulated through a mutation function. +Calling the mutation will then update the todo object in the app's local store for rendering in the view. + +For example, when marking a todo as done in the UI, the following flow occurs: + +- The `TodoItem.vue` calls the `markTodoDone()` function which dispatches an event to the store. +- This calls the `updateTodo()` function in the `actions.js` file +- The action will update the backend db (calling our `todolist api`) with our updated todo object. +- The action will commit the change to the store by calling the mutation method `MARK_TODO_COMPLETED` +- The `MARK_TODO_COMPLETED` will directly access the store object and update it with the new state value +- The `ListOfTodos.vue` component is watching the store for changes and when something gets updated it re-renders the `TodoItem.vue`. + +The implementation of our `important` flag will follow this same flow. + +1. Let's implement our feature by first creating a branch. Our new feature, important flag will behave in the same way as the `MARK_TODO_COMPLETED`. Create a new branch in your `todolist` app for our feature and push it to the remote + + ```bash + cd todolist + ``` + + ```bash + git checkout feature/important-flag + ``` + + ```bash + git push -u origin feature/important-flag + ``` + +1. Let's get our tests running by executing a `--watch` on our tests. This will keep re-running our tests everytime there is a file change. It is handy to have this running in a new terminal session. + + ```bash + npm run test:client -- --watch + ``` + +1. All the tests should be passing when we begin. If `No tests found related to files changed since last commit` is showing, hit `a` on the terminal to re-run `all` tests. + + ![rerun-all](../images/exercise3/rerun-all.png) + +1. We will add new tests in three places to validate our function behaves as defined in the Feature Story. + We will need to write tests for `TodoItem.vue` to verify that it has a clickable red flag. + Our app is going to need to persist the changes in the backend, so we will want to modify `actions.js` and `mutations.js` to keep the API and local copy of the store in sync. + Let's start with our `TodoItem.vue` component. Open the `tests/unit/vue-components/TodoItem.spec.js` file. + Notice that this file already contains a few tests that validate some of our AC's (acceptance criteria). + Find the `describe` block for our important flag tests. + The block contains a `beforeEach()` hook, which is a special function that is called before each test in the `describe` block is run. + + ![important-flag-before](../images/exercise3/important-flag-before.png) + +1. Each of our test cases has its skeleton in place already for example the `TodoItem.vue` component takes a property of `todos` when rendering. This setup is already done for each of our tests so all we have to do is fill in our assertions. + + ![todoitem-skeleton-tests](../images/exercise3/todoitem-skeleton-tests.png) + +1. Let's implement the first test `it("should render a button with important flag", ...)`. This test will assert that the button is present on the page and it contains the `.important-flag` CSS class. + To implement this, add the `expect` statement below the `// TODO - test goes here!` comment. + +📝 todolist/tests/unit/vue-components/TodoItem.spec.js + + + +#### ** Important Part ** + +```javascript +it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); +}); +``` + +#### ** Entire File ** + +```javascript + /* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' + +import * as all from "../setup.js"; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; + +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); + + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); + + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); + +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); + + it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); + }); + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); + it("call makImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); +}); +``` + + + +1. Save the file. Observe that the test case has started failing because we have not yet implemented the feature! + + ![todoitem-fail-test](../images/exercise3/todoitem-fail-test.png) + +1. With a basic assertion in place, let's continue on to the next few tests. We want the important flag to be red when an item in the todolist is marked important. + Conversely, we want it to not be red when important is false. + Complete the `expect` statements in your test file as shown below for both tests. + +📝 todolist/tests/unit/vue-components/TodoItem.spec.js + + + +#### ** Important Part ** + +```javascript + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(true); + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(false); + }); +``` + +#### ** Entire File ** -```bash -git push -``` +```javascript +/* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' -#### 2b - Create todolist front-end tests -> Using [Jest](https://facebook.github.io/jest/) as our test runner and the `vue-test-utils` library for managing our vue components; we will now write some tests for front end functionality to persist our important-flag. The changes required to the front end are quite large but we will use TDD to create our test first, then implement the functionality. +import * as all from "../setup.js"; -Our TodoList App uses `vuex` to manage the state of the app's todos and `axios` HTTP library to connect to the backend. `Vuex` is an opinionated framework for managing application state and has some key design features you will need to know to continue with the exercise. +const localVue = createLocalVue(); -In `vuex` the application state is managed by a `store`. The `store` houses all the todos we have retrieved from the backend as well as the `getter` methods for our array of `todos`. In order to make changes to the store, we could call the store directly and update each todo item but as earlier said; vuex is an opinionated module with its own way of updating the store. It is bad practice to call the store directly. +localVue.use(Vuex); -There are two parts of the lifecycle to updating the store, the `actions` & `mutations`. When the user clicks a todo to mark it as complete; the `actions` are called. An action could involve a call to the backend or some pre-processing of the data. Once this is done, the change is committed to the store by calling the `mutation` function. A store should only ever be manipulated through a mutation function. Calling the mutation will then update the todo object in the app's local store for rendering in the view. +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; -For example; when marking a todo as done in the UI, the following flow occurs - * The `TodoItem.vue` calls the `markTodoDone()` function which dispatches an event to the store. - * This calls the `updateTodo()` function in the `actions.js` file - * The action will update the backend db (calling our `todolist api`) with our updated todo object. - * The action will commit the change to the store by calling the mutation method `MARK_TODO_COMPLETED` - * The `MARK_TODO_COMPLETED` will directly access the store object and update it with the new state value - * The `ListOfTodos.vue` component is watching the store for changes and when something gets updated it re-renders the `TodoItem.vue`. +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); -The implementation of our `important` flag will follow this same flow. + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); -1. Let's implement our feature by first creating a branch. Our new feature, important flag will behave in the same way as the `MARK_TODO_COMPLETED`. Create a new branch in your `todolist` app for our feature and push it to the remote + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); -```bash -cd todolist -``` -```bash -git checkout feature/important-flag -``` -```bash -git push -u origin feature/important-flag -``` +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); -2. Let's get our tests running by executing a `--watch` on our tests. This will keep re-running our tests everytime there is a file change. It is handy to have this running in a new terminal session. -```bash -npm run test:client -- --watch + it("should render a button with important flag", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + expect(wrapper.find(".important-flag").exists()).toBe(true); + }); + it("should set the colour to red when true", () => { + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(true); + }); + it("should set the colour to not red when false", () => { + importantTodo.important = false; + const wrapper = mount(TodoItem, { + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + expect(wrapper.find(".red-flag").exists()).toBe(false); + }); + it("call makImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo } + }); + // TODO - test goes here! + }); +}); ``` -3. All the tests should be passing when we begin. If `No tests found related to files changed since last commit` is on show; hit `a` on the terminal to re-run `all` tests. + -![rerun-all](../images/exercise3/rerun-all.png) +1. Finally, we want to make the flag clickable and for it to call a function to update the state. The final test in the `TodoItem.spec.js` should simulate this behaviour. + Implement the `it("call markImportant when clicked", () ` test by first simulating the click of our important-flag and asserting the function `markImportant()` is executed. -4. We will add new tests in three places to validate our function behaves as expected against the acceptance criteria from the Feature Story supplied to us. We will need to write tests for our `TodoItem.vue` to verify having a red flag and that it is clickable. Our app is going to need to persist the changes in the backend so we'll want to make changes to our `actions.js` and `mutations.js` to keep the API and local copy of the store in sync. Let's start with our `TodoItem.vue` component. Open the `tests/unit/vue-components/TodoItem.spec.js` file. This has been templated with some example test to correspond with our A/Cs for speed of doing the exercise. Find the describe block for our important flag tests. It is set up already with a `beforeEach()` hook for test setup. -![important-flag-before](../images/exercise3/important-flag-before.png) +📝 todolist/tests/unit/vue-components/TodoItem.spec.js -5. Each of our test cases has its skeleton in place already for example the `TodoItem.vue` component takes a property of `todos` when rendering. This setup is already done for each of our tests so all we have to do is fill in our assertions. + -![todoitem-skeleton-tests](../images/exercise3/todoitem-skeleton-tests.png) +#### ** Important Part ** + +```javascript +it("call markImportant when clicked", () => { + const wrapper = mount(TodoItem, { + methods, + propsData: { todoItem: importantTodo }, + }); + // TODO - test goes here! + const input = wrapper.find(".important-flag"); + input.trigger("click"); + expect(methods.markImportant).toHaveBeenCalled(); +}); +``` -6. Let's implement the first test `it("should render a button with important flag"`. This test will assert if the button is present on the page and it contains the `.important-flag` CSS class. To implement this; add the `expect` statement as follows below the `// TODO - test goes here!` comment. +#### ** Entire File ** -📝 todolist/tests/unit/vue-components/TodoItem.spec.js ```javascript +/* eslint-disable */ +import { shallow, mount, createLocalVue } from "@vue/test-utils"; +import Vuex from "vuex"; +import TodoItem from "@/components/TodoItem.vue"; +// import { expect } from 'chai' + +import * as all from "../setup.js"; + +const localVue = createLocalVue(); + +localVue.use(Vuex); + +const todoItem = { + title: "Love Front End testing :)", + completed: true +}; + +describe("TodoItem.vue", () => { + it("has the expected html structure", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + // expect(wrapper.element).toMatchSnapshot(); + }); + + it("Renders title as 'Love Front End testing :)'", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.title).toMatch("Love Front End testing :)"); + }); + + it("Renders completed as true", () => { + const wrapper = shallow(TodoItem, { + propsData: { todoItem } + }); + expect(wrapper.vm.todoItem.completed).toEqual(true); + }); +}); + +let importantTodo; +let methods; + +describe("Important Flag button ", () => { + beforeEach(() => { + importantTodo = { + title: "Love Front End testing :)", + completed: true, + important: true + }; + methods = { markImportant: jest.fn() }; + }); + it("should render a button with important flag", () => { const wrapper = mount(TodoItem, { propsData: { todoItem: importantTodo } @@ -473,19 +1892,9 @@ npm run test:client -- --watch // TODO - test goes here! expect(wrapper.find(".important-flag").exists()).toBe(true); }); -``` - -7. Save the file. Observe that the test case has started failing because we have not yet implemented the feature! - -![todoitem-fail-test](../images/exercise3/todoitem-fail-test.png) - -8. With a basic assertion in place, let's continue on to the next few tests. We want the important flag to be red when an item in the todolist is marked accordingly. Conversely we want it to be not red when false. Let's create a check for `.red-flag` CSS property to be present when important is true and not when false. Complete the `expect` statements in your test file as shown below for both tests. - -📝 todolist/tests/unit/vue-components/TodoItem.spec.js -```javascript it("should set the colour to red when true", () => { const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(true); @@ -493,39 +1902,41 @@ npm run test:client -- --watch it("should set the colour to not red when false", () => { importantTodo.important = false; const wrapper = mount(TodoItem, { - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! expect(wrapper.find(".red-flag").exists()).toBe(false); }); -``` - -9. Finally, we want to make the flag clickable and for it to call a function to update the state. The final test in the `TodoItem.spec.js` we want to create should simulate this behaviour. Implement the `it("call markImportant when clicked", () ` test by first simulating the click of our important-flag and asserting the function `markImportant()` to write is executed. - -📝 todolist/tests/unit/vue-components/TodoItem.spec.js -```javascript it("call markImportant when clicked", () => { const wrapper = mount(TodoItem, { methods, - propsData: { todoItem: importantTodo } + propsData: { todoItem: importantTodo }, }); // TODO - test goes here! const input = wrapper.find(".important-flag"); input.trigger("click"); expect(methods.markImportant).toHaveBeenCalled(); }); +}); ``` -10. With our tests written for the feature's UI component, let's implement our code to pass the tests. Explore the `src/components/TodoItem.vue`. Each vue file is broken down into 3 sections + - * The `` contains the HTML of our component. This could include references to other Components also - * The `` contains the JavaScript of our component and is essentially the logic for our component. It defines things like `properties`, `methods` and other `components` - * The `` contains the encapsulated CSS of our component +1. With our tests written for the feature's UI component, let's implement our code to pass the tests. Open `src/components/TodoItem.vue`. Each vue file is broken down into 3 sections -11. Underneath the `` tag, let's add a new md-button. Add an `.important-flag` class on the `md-button` and put the svg of the flag provided inside it. + - The `` contains the HTML of our component. This could include references to other Components also + - The `` contains the JavaScript of our component and is essentially the logic for our component. It defines things like `properties`, `methods` and other `components` + - The `` contains the encapsulated CSS of our component -📝 todolist/src/components/TodoItem.vue -```html +1. Underneath the `` tag, let's add a new md-button. Add the `.important-flag` class on the `md-button` and put the provided svg of the flag inside it. + +📝 todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** + + ```html @@ -533,24 +1944,217 @@ npm run test:client -- --watch ``` -12. We should now see the first of our failing tests has started to pass. Running the app locally (using `npm run serve`) should show the flag appear in the UI. It is clickable but won't fire any events and the colour is not red as per our requirement. +#### ** Entire File ** -

-NOTE - If you don't see the important flag in the CRW UI preview window, an old UI version might be cached. Try to use the external link provided by CRW. -

+```html + + + + + +``` + + + +1. We should now see the first of our failing tests has started to pass. + Running the app locally (using `npm run serve`) should show the flag in the UI. + It is clickable, but it won't fire any events. Additionally, the colour is not red as required in the acceptance criteria. + +

+ NOTE - If you don't see the important flag in the CRW UI preview window, an old UI version might be cached. Try to use the external link provided by CRW. +

-Let's continue to implement the colour change for the flag. On our `` tag, add some logic to bind the css to the property of a `todo.important` by adding ` :class="{'red-flag': todoItem.important}" `. This logic will apply the CSS class when `todo.important` is true. + Let's continue to implement the colour change for the flag. On our `` tag, add some logic to bind the css to the property of a `todo.important` by adding `:class="{'red-flag': todoItem.important}" `. This logic will apply the CSS class when `todo.important` is true. + +📝 todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** -📝 todolist/src/components/TodoItem.vue ```html - - - + + + + + + +``` + +#### ** Entire File ** + +```html + + + + + ``` -13. More tests should now be passing. Let's wire the click of the flag to an event in Javascript. In the methods section of the `` tags in the Vue file, implement the `markImportant()`. We want to wire this to the action to updateTodo, just like we have in the `markCompleted()` call above it. We also need to pass an additional property to this method called `important` + + +1. More tests should now be passing. Let's wire the click of the flag to an event in Javascript. + In the methods section of the `` tags in the Vue file, implement `markImportant()`. + We want to wire this to the action `updateTodo`, just like we have in the `markCompleted()` call. + We also need to pass an additional property to this method called `important` + +📝 todolist/src/components/TodoItem.vue + + + +#### ** Important Part ** -📝 todolist/src/components/TodoItem.vue ```javascript markImportant() { // TODO - FILL THIS OUT IN THE EXERCISE @@ -559,249 +2163,455 @@ Let's continue to implement the colour change for the flag. On our `` tag, } ``` -14. Let's connect the click button in the DOM to the Javascript function we've just created. In the template, add a click handler to the md-button to call the function `markImportant()` by adding ` @click="markImportant()"` to the `` tag +#### ** Entire File ** -📝 todolist/src/components/TodoItem.vue ```html - - - - -``` + + + + + ``` -17. Start our local development server (if not already running) to manually verify the changes + -```bash -npm run serve:all -``` +1. Let's connect the click button in the DOM to the Javascript function we've just created. + In the template, add a click handler to the md-button to call the function `markImportant()` by adding ` @click="markImportant()"` to the `` tag. -18. Open our local todolist app (http://localhost:8080/#/todo). If we try to use our important flag, we should see it's still not behaving as expected; this is because we're not updating the state of the app in response to the click event. +📝 todolist/src/components/TodoItem.vue -19. We need to implement the `actions` and `mutations` for our feature. Let's start with the tests. Open the `tests/unit/javascript/actions.spec.js` and navigate to the bottom of the file. Our action should should commit the `MARK_TODO_IMPORTANT` to the mutations. Scroll to the end of the test file and implement the skeleton test by adding `expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT");` as the assertion. + -📝 todolist/tests/unit/javascript/actions.spec.js -```javascript - it("should call MARK_TODO_IMPORTANT", done => { - const commit = sinon.spy(); - state.todos = todos; - actions.updateTodo({ commit, state }, { id: 1, important: true }).then(() => { - // TODO - test goes here! - expect(commit.firstCall.args[0]).toBe("MARK_TODO_IMPORTANT"); - done(); - }); - }); +#### ** Important Part ** + +```html + + + + + + + ``` -20. We should now have more failing tests, let's fix this by adding the call from our action to the mutation method. Open the `src/store/actions.js` file and scroll to the bottom to the `updateTodo()` method. Complete the if block by adding `commit("MARK_TODO_IMPORTANT", i);` as shown below. +#### ** Entire File ** -📝 todolist/src/store/actions.js -```javascript -updateTodo({ commit, state }, { id, important }) { - let i = state.todos.findIndex(todo => todo._id === id); - if (important) { - // TODO - add commit important here! - commit("MARK_TODO_IMPORTANT", i); - } else { - commit("MARK_TODO_COMPLETED", i); +```html + + + + + ``` -22. With our tests running and failing, let's implement the feature to their spec. Open the `src/store/mutations.js` and add another function called `MARK_TODO_IMPORTANT` below the `MARK_TODO_COMPLETED` to toggle `todo.important` between true and false. + -NOTE - add a `,` at the end of the `MARK_TODO_COMPLETED(){}` function call. +1. All of our tests should now be passing. On the watch tab where they are running, hit `a` to re-run all tests and update any snapshots with `u` if needed. -📝 todolist/src/store/mutations.js -```javascript - MARK_TODO_IMPORTANT(state, index) { - console.log("INFO - MARK_TODO_IMPORTANT"); - state.todos[index].important = !state.todos[index].important; - } -``` +1. With all of our tests now passing, let's commit our code. On the terminal, run -![mark-todo-important](../images/exercise3/mark-todo-important.png) + ```bash + git add . + ``` -23. All our tests should now be passing. On the watch tab where they are running, hit `a` to re-run all tests and update any snapshots with `u` if needed. + ```bash + git commit -m "Implementing the store and actions" + ``` -24. With all our tests now passing, let's commit our code. On the terminal, run + ```bash + git push + ``` -```bash -git add . -``` -```bash -git commit -m "Implementing the store and actions" -``` -```bash -git push -``` +1. Before running a build in Jenkins, let's add our tests and code to the `develop` branch. + Ask your neighbour to review your code changes and, if they approve, merge them to `develop`! (If you're feeling adventurous, raise a PR through GitLab and have someone else peer review it!) -25. Before running a build in Jenkins, let's add our tests and code to the develop branch. Ask your neighbour to review your code changes and if they approve; merge them to Develop! (If you're feeling adventurous - raise a PR through GitLab and have someone on your desk peer review it!) + ```bash + git checkout develop + ``` -```bash -git checkout develop -``` -```bash -git merge feature/important-flag -``` + ```bash + git merge feature/important-flag + ``` -When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. + When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. -```bash -git push --all -``` + ```bash + git push --all + ``` #### 2c - Create todolist e2e tests -> _Using [Nightwatch.js](http://nightwatchjs.org/) We will write a reasonably simple e2e test to test the functionality of the feature we just implemented._ +Using [Nightwatch.js](http://nightwatchjs.org/) We will write a simple e2e test to test the functionality of the feature we just implemented. -1. Firstly we need to create an e2e spec test file in the correct place. +1. First, we need to create an e2e test file. -```bash -touch tests/e2e/specs/importantFlag.js -``` + ```bash + touch tests/e2e/specs/importantFlag.js + ``` -2. Open this new file in your code editor and set out the initial blank template for an e2e test as below: +1. Open this new file in your code editor and create an initial template for an e2e test as shown below: -📝 todolist/tests/e2e/specs/importantFlag.js -```javascript - module.exports = { - "Testing important flag setting": browser => { + 📝 todolist/tests/e2e/specs/importantFlag.js - } - }; -``` -![if-e2e-step1](../images/exercise3/if-e2e-step1.png) + ```javascript + module.exports = { + "Testing important flag setting": browser => { -3. Now get the test to access the todos page and wait for it to load. The url can be taken from `process.env.VUE_DEV_SERVER_URL` + } + } + ``` - ![if-e2e-step2](../images/exercise3/if-e2e-step2.png) +1. Now, get the test to access the todos page and wait for it to load. The url can be taken from `process.env.VUE_DEV_SERVER_URL` -4. Write code to do the following: - * Click the clear all button and then enter a value in the textbox to create a new item. - * Assert that an 'important flag' exists (the button to set important) - * Assert that a red flag does not exist. - * Click the important flag and check whether the flag has turned red. +📝 todolist/tests/e2e/specs/importantFlag.js -5. You will need to reference the clear all button from the test code. We will therefore have to go to `src/components/XofYItems.vue` and add `id="clear-all"` to the clear all button. + - - ![if-e2e-step3a](../images/exercise3/if-e2e-step3a.png) +#### ** Important Part ** -📝 src/components/XofYItems.vue -```html - +```javascript +browser + .url(process.env.VUE_DEV_SERVER_URL + "/#/todo") + .waitForElementVisible("body", 5000); ``` -6. Write the following test code. The pauses allow for the body of the page to render the todo list before exercising the test code: +#### ** Entire File ** -📝 todolist/tests/e2e/specs/importantFlag.js ```javascript - module.exports = { - "Testing important flag setting": browser => { - browser - .url(process.env.VUE_DEV_SERVER_URL + '/#/todo') - .waitForElementVisible("body", 5000) - .pause(5000) - .click('#clear-all') - .pause(2000) - .setValue('input',['set a todo',browser.Keys.ENTER]) - .pause(2000) - .assert.elementPresent(".important-flag") - .assert.elementNotPresent(".red-flag") - .click('.important-flag') - .end(); - } - }; +module.exports = { + "Testing important flag setting": (browser) => { + browser + .url(process.env.VUE_DEV_SERVER_URL + "/#/todo") + .waitForElementVisible("body", 5000); + }, +}; ``` -7. Your final E2E test should look like the following: + - ![if-e2e-step4](../images/exercise3/e2e-code-listing-full.jpg) +1. Write code to do the following: -8. At this point we should have a working e2e test. We can run this by using `npm run e2e:ide` (make sure you have the app running locally with `npm run serve:all` and that selenium is running in the background with `npm run selenium`). When all tests pass, we can push up these changes. + - Click the clear all button and then enter a value in the textbox to create a new item. + - Assert that an 'important flag' exists (the button to set important) + - Assert that a red flag does not exist. + - Click the important flag and check whether the flag has turned red. -```bash -git add . -``` +1. You will need to reference the clear all button from the test code. We will therefore have to go to `src/components/XofYItems.vue` and add `id="clear-all"` to the clear all button. -```bash -git commit -m "Implementing e2e tests" -``` +📝 src/components/XofYItems.vue -```bash -git push -``` + -9. Once the pipeline on the `develop` branch runs successfully, let's merge our updates back into the main branch. +#### ** Important Part ** -```bash -git checkout master -``` -```bash -git merge develop +```html + ``` -When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. +#### ** Entire File ** -```bash -git push --all +```html + + + + + + ``` + + +1. Complete your test code by writing the following steps. The pauses allow for the body of the page to render the todo list before executing the test code. Your final e2e test should look like this: + +📝 todolist/tests/e2e/specs/importantFlag.js + +```javascript +module.exports = { + "Testing important flag setting": (browser) => { + browser + .url(process.env.VUE_DEV_SERVER_URL + "/#/todo") + .waitForElementVisible("body", 5000) + .pause(5000) + .click("#clear-all") + .pause(2000) + .setValue("input", ["set a todo", browser.Keys.ENTER]) + .pause(2000) + .assert.elementPresent(".important-flag") + .assert.elementNotPresent(".red-flag") + .click(".important-flag") + .assert.elementPresent(".red-flag") + .end(); + }, +}; +``` + +1. At this point we should have a working e2e test. We can run this by using `npm run e2e:ide` (make sure you have the app running locally with `npm run serve:all` and that selenium is running in the background with `npm run selenium`). When all tests pass, we can push up these changes. + + ```bash + git add . + ``` + + ```bash + git commit -m "Implementing e2e tests" + ``` + + ```bash + git push + ``` + +1. Once the pipeline on the `develop` branch runs successfully, let's merge our updates back into the main branch. + + ```bash + git checkout master + ``` + + ```bash + git merge develop + ``` + + When the editor screen appears in the terminal after running the merge, type `:q` and hit enter to quit the editor. + + ```bash + git push --all + ``` + --- ## Extension Tasks > _Ideas for go-getters. Advanced topic for doers to get on with if they finish early. These will usually not have a solution and are provided for additional scope._ -* Add Config maps to inject DB creds to the app -* Create a blue/green deploy based on the success of running e2e tests against either blue or green and flopping load over to new endpoint when successful. -Advanced -* Add Auth0 support to your FE app. +- Add Config maps to inject DB creds to the app +- Create a blue/green deploy based on the success of running e2e tests against either blue or green and flopping load over to new endpoint when successful. + Advanced +- Add Auth0 support to your FE app. - + +#### ** Important Part ** + ```yaml # JENKINS AGENTS - object: jenkins-agent-nodes @@ -83,6 +88,81 @@ _____ - jenkins-agents - zap-agent ``` + +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +# JENKINS AGENTS +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent + +``` + + + ![zap-object](../images/exercise4/zap-object.png) 4. Run the ansible playbook filtering with tag `zap` so only the zap build pods are run. @@ -107,21 +187,110 @@ ansible-playbook apply.yml -e target=tools \ 1. Create an object in `inventory/host_vars/ci-cd-tooling.yml` called `arachni` and add the following variables to tell your template where to find the agent definition to be built. -📝 *inventory/host_vars/ci-cd-tooling.yml* +📝 inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + ```yaml +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" +``` - arachni: - SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" - SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni - BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 - NAME: jenkins-agent-arachni - SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent ``` + + + ![arachni-object-parameters](../images/exercise4/arachni-object-parameters.png) 2. Add the definition below underneath the Zap config -📝 *inventory/host_vars/ci-cd-tooling.yml* +📝 inventory/host_vars/ci-cd-tooling.yml + + + +#### ** Important Part ** + ```yaml - name: jenkins-agent-arachni template: "{{ cop_quickstarts_raw }}/{{ cop_quickstarts_raw_version_tag }}/.openshift/templates/jenkins-agent-generic-template.yml" @@ -131,6 +300,93 @@ ansible-playbook apply.yml -e target=tools \ - jenkins-agents - arachni-agent ``` + +#### ** Entire File ** + +```yaml +--- +ci_cd: + IMAGE_STREAM_NAMESPACE: "{{ ci_cd_namespace }}" + +arachni: + SOURCE_REPOSITORY_URL: "{{ cop_quickstarts }}" + SOURCE_CONTEXT_DIR: jenkins-agents/jenkins-agent-arachni + BUILDER_IMAGE_NAME: quay.io/openshift/origin-jenkins-agent-base:4.5 + NAME: jenkins-agent-arachni + SOURCE_REPOSITORY_REF: "{{ cop_quickstarts_raw_version_tag }}" + +ansible_connection: local + +openshift_cluster_content: +- galaxy_requirements: + - "{{ inventory_dir }}/../exercise-requirements.yml" +- object: ci-cd-tooling + content: + - name: "nexus" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/nexus/nexus-deployment-template.yml" + params: "{{ playbook_dir }}/params/nexus" + tags: + - nexus + - name: "jenkins-mongodb" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/mongodb-ephemeral.yml" + params: "{{ playbook_dir }}/params/mongodb" + tags: + - mongodb + - name: "jenkins" + namespace: "{{ ci_cd_namespace }}" + template: "{{ openshift_templates_raw }}/{{ openshift_templates_raw_version_tag }}/jenkins/jenkins-persistent-template.yml" + params: "{{ playbook_dir }}/params/jenkins" + post_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/enablement-npm:latest" + img_tag: "jenkins-agent-npm:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-npm" + tags: + - jenkins +- object: ci-cd-builds + content: + - name: "jenkins-s2i" + namespace: "{{ ci_cd_namespace }}" + template: "{{ playbook_dir }}/templates/jenkins-s2i-build-template-with-secret.yml" + params: "{{ playbook_dir }}/params/jenkins-s2i" + params_from_vars: "{{ ci_cd }}" + tags: + - jenkins +- object: jenkins-agent-nodes + content: + - name: jenkins-agent-zap + namespace: "{{ ci_cd_namespace }}" + pre_steps: + - role: casl-ansible/roles/openshift-imagetag + vars: + source_img: "quay.io/rht-labs/jenkins-slave-zap:do500.v2" + img_tag: "jenkins-agent-zap:latest" + - role: casl-ansible/roles/openshift-labels + vars: + label: "role=jenkins-slave" + target_object: "imagestream" + target_name: "jenkins-agent-zap" + tags: + - jenkins-agents + - zap-agent + - name: jenkins-agent-arachni + template: "{{ cop_quickstarts_raw }}/{{ cop_quickstarts_raw_version_tag }}/.openshift/templates/jenkins-agent-generic-template.yml" + params_from_vars: "{{ arachni }}" + namespace: "{{ ci_cd_namespace }}" + tags: + - jenkins-agents + - arachni-agent +``` + + + ![arachni-object](../images/exercise4/arachni-object.png) 3. Run the ansible playbook filtering with tag `arachni` so only the arachni build pods are run. @@ -175,7 +431,12 @@ NAME=todolist 4. Create a new object in `.openshift-applier/inventory/group_vars/all.yml` to drive the `ocp-pipeline` template with the parameters file you've just created. It can be put under the existing `todolist-build` object. -📝 *.openshift-applier/inventory/group_vars/all.yml* +📝 .openshift-applier/inventory/group_vars/all.yml + + + +#### ** Important Part ** + ```yaml - name: todolist-pipeline template: "{{ playbook_dir }}/templates/ocp-pipeline.yml" @@ -184,6 +445,83 @@ NAME=todolist tags: - pipeline ``` + +#### ** Entire File ** + +```yaml +--- +app_name: todolist + +build: + NAME: '{{ app_name }}' +deploy: + dev: + PIPELINES_NAMESPACE: '{{ ci_cd_namespace }}' + NAME: '{{ app_name }}' + NAMESPACE: '{{ dev_namespace }}' + DEPLOYER_USER: jenkins + NODE_ENV: dev + test: + PIPELINES_NAMESPACE: '{{ ci_cd_namespace }}' + NAME: '{{ app_name }}' + NAMESPACE: '{{ test_namespace }}' + DEPLOYER_USER: jenkins + NODE_ENV: test + +db: + MONGODB_DATABASE: '{{ app_name }}' + +openshift_cluster_content: + - object: app-builds + content: + - name: todolist-build + template: '{{ playbook_dir }}/templates/todolist-build.yml' + params_from_vars: '{{ build }}' + namespace: '{{ ci_cd_namespace }}' + tags: + - build + - name: todolist-pipeline + template: "{{ playbook_dir }}/templates/ocp-pipeline.yml" + params: "{{ playbook_dir }}/params/ocp-pipeline" + namespace: "{{ ci_cd_namespace }}" + tags: + - pipeline + - object: deploy-dev + content: + - name: todolist + template: '{{ playbook_dir }}/templates/todolist-deploy.yml' + params_from_vars: '{{ deploy.dev }}' + namespace: '{{ dev_namespace }}' + tags: + - deploy + - dev + - name: todolist-db + template: '{{ playbook_dir }}/templates/mongodb.yml' + params_from_vars: '{{ db }}' + namespace: '{{ dev_namespace }}' + tags: + - deploy + - dev + - object: deploy-test + content: + - name: todolist + template: '{{ playbook_dir }}/templates/todolist-deploy.yml' + params_from_vars: '{{ deploy.test }}' + namespace: '{{ test_namespace }}' + tags: + - deploy + - test + - name: todolist-db + template: '{{ playbook_dir }}/templates/mongodb.yml' + params_from_vars: '{{ db }}' + namespace: '{{ test_namespace }}' + tags: + - deploy + - test +``` + + + ![ocp-pipeline-applier](../images/exercise4/ocp-pipeline-applier.png) 5. Log in to OpenShift using the `oc` client, and use the OpenShift Applier to create the cluster content diff --git a/exercises/5-non-functionals-strike-back/README.md b/exercises/5-non-functionals-strike-back/README.md index 8d59a255..77d4eef6 100644 --- a/exercises/5-non-functionals-strike-back/README.md +++ b/exercises/5-non-functionals-strike-back/README.md @@ -74,7 +74,12 @@ _____ 3. Create a new stage called `Security Scan` underneath the `stage("e2e test") { }` section as shown below. This is a parallel stage which will allow us to define additional `stages() {}` inside of it. We will add two stages in there, one for `Zap` and one for `Arachni`. The contents of the `e2e test` have been removed for simplicity. -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage("e2e test") { // ... stuff in here .... @@ -91,9 +96,239 @@ _____ } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 4. Let's start filling out the configuration for the OWASP Zap scan first. We will set the label to our agent created in previous exercise and a `when` condition to only execute the job when on either the master or develop branch. -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -107,9 +342,246 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 5. Add a `step` with a `sh` command to run the tool by passing in the URL of the app we're going to test. -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -130,9 +602,254 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + } + stage('Arachni') { + + } + } + } + } +} + +``` + + + 6. Finally add the reporting for Jenkins in `post` hook of our Declarative Pipeline. This is to report the findings of the scan in Jenkins as an HTML report. -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('OWASP Scan') { agent { @@ -166,9 +883,266 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + + } + } + } + } +} +``` + + + 7. Let's add our Arachni Scan to the second part of the parallel block. The main difference between these sections is Jenkins will report an XML report too for failing the build accordingly. Below is the snippet for the Arachni scanning. -📝 *todolist/Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy stage('Arachni') { agent { @@ -202,6 +1176,285 @@ stage('OWASP Scan') { } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 8. With this config in place, commit your code (from your terminal). Wait for a few minutes until a new build in Jenkins is triggered: ```bash @@ -236,7 +1489,12 @@ NOTE - your build may have failed, or marked as unstable because of the a securi 2. Open the `Jenkinsfile` in the root of the project; move to the `stage("node-build"){ ... }` section. In the `post` section add a block for producing a `HTML` report as part of our builds. This is all that is needed for Jenkins to report the coverage stats. -📝 *Jenkinsfile* +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy // Post can be used both on individual stages and for the entire build. post { @@ -255,7 +1513,300 @@ NOTE - your build may have failed, or marked as unstable because of the a securi } ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'reports/coverage', + reportFiles: 'index.html', + reportName: 'Code Coverage' + ] + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 3. To get the linting working; we will add a new step to our `stage("node-build"){ }` section to lint the JavaScript code. Continuing in the `Jenkinsfile`, After the `npm install`; add a command to run the linting. + +📝 todolist/Jenkinsfile + + + +#### ** Important Part ** + ```groovy echo '### Install deps ###' sh 'npm install' @@ -263,6 +1814,295 @@ echo '### Running linting ###' sh 'npm run lint' ``` +#### ** Entire File ** + +```groovy +pipeline { + + agent { + // label "" also could have been 'agent any' - that has the same meaning. + label "master" + } + + environment { + // Global Vars + NAMESPACE_PREFIX="" + GITLAB_DOMAIN = "" + GITLAB_USERNAME = "" + + PIPELINES_NAMESPACE = "${NAMESPACE_PREFIX}-ci-cd" + APP_NAME = "todolist" + + JENKINS_TAG = "${JOB_NAME}.${BUILD_NUMBER}".replace("/", "-") + JOB_NAME = "${JOB_NAME}".replace("/", "-") + + GIT_SSL_NO_VERIFY = true + GIT_CREDENTIALS = credentials("${NAMESPACE_PREFIX}-ci-cd-git-auth") + } + + // The options directive is for configuration that applies to the whole job. + options { + buildDiscarder(logRotator(numToKeepStr:'5')) + timeout(time: 15, unit: 'MINUTES') + ansiColor('xterm') + timestamps() + } + + stages { + stage("prepare environment for master deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-test" + env.NODE_ENV = "test" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("prepare environment for develop deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*develop)/ } + } + steps { + script { + // Arbitrary Groovy Script executions can do in script tags + env.PROJECT_NAMESPACE = "${NAMESPACE_PREFIX}-dev" + env.NODE_ENV = "dev" + env.E2E_TEST_ROUTE = "oc get route/${APP_NAME} --template='{{.spec.host}}' -n ${PROJECT_NAMESPACE}".execute().text.minus("'").minus("'") + } + } + } + stage("node-build") { + agent { + node { + label "jenkins-agent-npm" + } + } + steps { + sh 'printenv' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running linting ###' + sh 'npm run lint' + + echo '### Running tests ###' + sh 'npm run test:all:ci' + + echo '### Running build ###' + sh 'npm run build:ci' + + echo '### Packaging App for Nexus ###' + sh 'npm run package' + sh 'npm run publish' + stash 'source' + } + // Post can be used both on individual stages and for the entire build. + post { + always { + archive "**" + // ADD TESTS REPORTS HERE + junit 'test-report.xml' + junit 'reports/server/mocha/test-results.xml' + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'reports/coverage', + reportFiles: 'index.html', + reportName: 'Code Coverage' + ] + } + success { + echo "Git tagging" + script { + env.ENCODED_PSW=URLEncoder.encode(GIT_CREDENTIALS_PSW, "UTF-8") + } + sh''' + git config --global user.email "jenkins@jmail.com" + git config --global user.name "jenkins-ci" + git tag -a ${JENKINS_TAG} -m "JENKINS automated commit" + git push https://${GIT_CREDENTIALS_USR}:${ENCODED_PSW}@${GITLAB_DOMAIN}/${GITLAB_USERNAME}/${APP_NAME}.git --tags + ''' + } + failure { + echo "FAILURE" + } + } + } + + stage("node-bake") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + echo '### Get Binary from Nexus ###' + sh ''' + rm -rf package-contents* + curl -v -f http://admin:admin123@${NEXUS_SERVICE_HOST}:${NEXUS_SERVICE_PORT}/repository/zip/com/redhat/todolist/${JENKINS_TAG}/package-contents.zip -o package-contents.zip + unzip package-contents.zip + ''' + echo '### Create Linux Container Image from package ###' + sh ''' + oc project ${PIPELINES_NAMESPACE} # probs not needed + oc patch bc ${APP_NAME} -p "{\\"spec\\":{\\"output\\":{\\"to\\":{\\"kind\\":\\"ImageStreamTag\\",\\"name\\":\\"${APP_NAME}:${JENKINS_TAG}\\"}}}}" + oc start-build ${APP_NAME} --from-dir=package-contents/ --follow + ''' + } + // this post step chews up space. uncomment if you want all bake artefacts archived + // post { + //always { + // archive "**" + //} + //} + } + + stage("node-deploy") { + agent { + node { + label "master" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + script { + openshift.withCluster() { + openshift.withProject("${PROJECT_NAMESPACE}") { + echo '### tag image for namespace ###' + openshift.tag("${PIPELINES_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}", "${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### set env vars and image for deployment ###' + openshift.raw("set","env","dc/${APP_NAME}","NODE_ENV=${NODE_ENV}") + openshift.raw("set", "image", "dc/${APP_NAME}", "${APP_NAME}=image-registry.openshift-image-registry.svc:5000/${PROJECT_NAMESPACE}/${APP_NAME}:${JENKINS_TAG}") + + echo '### Rollout and Verify OCP Deployment ###' + openshift.selector("dc", "${APP_NAME}").rollout().latest() + openshift.selector("dc", "${APP_NAME}").rollout().status("-w") + openshift.selector("dc", "${APP_NAME}").scale("--replicas=1") + openshift.selector("dc", "${APP_NAME}").related('pods').untilEach("1".toInteger()) { + return (it.object().status.phase == "Running") + } + } + } + } + } + } + stage("e2e test") { + agent { + node { + label "jenkins-agent-npm" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + unstash 'source' + + echo '### Install deps ###' + sh 'npm install' + + echo '### Running end to end tests ###' + sh 'npm run e2e:jenkins' + } + post { + always { + junit 'reports/e2e/specs/*.xml' + } + } + } + stage('Security Scan') { + parallel { + stage('OWASP Scan') { + agent { + node { + label "jenkins-agent-zap" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + export REPORT_DIR="$WORKSPACE/" + /zap/zap-baseline.py -r index.html -t https://${E2E_TEST_ROUTE} || return_code=$? + echo "exit value was - " $return_code + ''' + } + post { + always { + // publish html + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: '', + reportFiles: 'index.html', + reportName: 'Zap Branniscan' + ] + } + } + } + stage('Arachni') { + agent { + node { + label "jenkins-agent-arachni" + } + } + when { + expression { GIT_BRANCH ==~ /(.*master|.*develop)/ } + } + steps { + sh ''' + /arachni/bin/arachni https://${E2E_TEST_ROUTE} --report-save-path=arachni-report.afr + /arachni/bin/arachni_reporter arachni-report.afr --reporter=xunit:outfile=report.xml --reporter=html:outfile=web-report.zip + unzip web-report.zip -d arachni-web-report + ''' + } + post { + always { + junit 'report.xml' + publishHTML target: [ + allowMissing: false, + alwaysLinkToLastBuild: false, + keepAll: true, + reportDir: 'arachni-web-report', + reportFiles: 'index.html', + reportName: 'Arachni Web Crawl' + ] + } + } + } + } + } + } +} +``` + + + 4. Save the `Jenkinsfile` and commit it to trigger a build with some more enhancements. ```bash git add . @@ -279,13 +2119,102 @@ git push 6. Fix the error identified by the linter by commenting out the offending line. -📝 src/components/TodoItem.vue -```html +📝 src/components/TodoItem.vue + + + +#### ** Important Part ** + +```javascript Vue.component("checkbox", Checkbox); Vue.component("radio", Radio); // let biscuits; ``` +#### ** Entire File ** + +```html + + + + + + + +``` + + + 7. Save the `TodoItem.vue` and Commit and push your changes to trigger a new build. ```bash diff --git a/exercises/index.html b/exercises/index.html index 54a37887..bf240ed0 100644 --- a/exercises/index.html +++ b/exercises/index.html @@ -24,7 +24,14 @@ subMaxLevel: 5, auto2top: true, search: 'auto', - notFoundPage: 'notThe404.md' + notFoundPage: 'notThe404.md', + tabs: { + persist : false, + sync : false, + theme : 'classic', + tabComments: true, + tabHeadings: true + } } @@ -35,6 +42,7 @@ +