# GitLab CI / CD et documentation

![](README_assets/pipeline.png)

> Environnement de développement `linux`. Outils et programmes utilisés :
>
> * [Git](https://git-scm.com/downloads)
> * [docker](https://docs.docker.com/v17.12/install/)
> * [nodejs](https://nodejs.org/en/download/)
> * [npm](https://www.npmjs.com/get-npm)
> * [python3](https://www.python.org/downloads/)
> * [pip3](https://pip.pypa.io/en/stable/installing/)
> * [virtualenv](https://virtualenv.pypa.io/en/stable/installation/)

## Introduction

Pour suivre ce tutoriel, il faut commencer par `fork` ce dépôt GitLab [https://githepia.hesge.ch/cours-gl/atelier-gitlab-ci-cd](https://githepia.hesge.ch/cours-gl/atelier-gitlab-ci-cd) pour avoir une copie du projet dans votre namespace, et avoir le contenu en local sur votre machine de travail (`git clone ...`)

Ce dépôt contient une application [**Node.js**](https://nodejs.org) conçue pour ce tutoriel : une simple calculatrice qui effectue des additions et des soustractions, et qui enregistre l'historique des opérations dans une base de donnée [**MongoDB**](https://www.mongodb.com). Le code contient des tests unitaires utilisant [**Jest.js**](https://jestjs.io/). On peut aussi générer de la documentation utilisant la syntaxe [**JSDoc**](https://jsdoc.app) et l'outil de génération [**documentation.js**](https://documentation.js.org).

Voici la structure du projet :

![](README_assets/structure.png)

## Installation et utilisation

### 		Mise en place de la base de données

Pour lancer l'application, il faut 

1. avoir une base de données **MongoDB** qui tourne. Pour cela, Nous vous proposons de lancer un container docker pour la base de données et exposer le port `3001` :

   ```shell
   docker run --name mongo_gl -d -p 3001:27017 mongo:3.4
   ```

   Pour vérifier que le container fonctionne, taper la commande suivante `docker ps`, un container ayant le nom `mongo_gl` doit exister :

   ![](README_assets/docker_mongodb.png) 

1. Dans la racine du projet, créer une copie du fichier `.env_example` et nommer le `.env`. Ce fichier va contenir les variables d'environnement utilisées par l'application **Node.js** (l'adresse ip et le port de la base de données par exemple).

3. Modifier le contenue de ce fichier pour qu'il s'adapte à votre base de données **MongoDB** en mettant les bonnes valeurs. Par exemple, si vous avez choisi que votre base de données tourne sur le port `3001` et que le nom de la base de données soit `tuto_gl`, alors le contenu du fichier `.env` sera le suivant : `DATABASE=mongodb://localhost:3001/tuto_gl`

### Lancer l'application Node.js

Toujours dans la racine du projet GitLab :

1. Installer les dépendances : 

   ```shell
   npm install
   ```

2. Lancer l'application :

   ```shell
   npm run watch
   ```

   Cette commande permet de lancer l'application utilisant l'outil `nodemon` qui va relancer l'exécution après chaque modification dans un des fichiers dans le dossier de l'application. Cet outil est très utile durant la phase de développement.

   L'application est donc lancée sur le port `3000`. Dans un navigateur, visitez l'adresse [localhost:3000](localhost:3000) :

   ![](README_assets/site.png)

   Essayez d’effectuer quelques opérations d'addition et de soustraction. Après quelques essaies, vous allez remarquer que les résultats des additions ne sont pas corrects (sciemment). Nous allons corriger ça plus tard dans le tutoriel.

### Lancer les tests unitaires `Jest`

Trois tests unitaires pour chaque opération arithmétique (addition et soustraction) sont implémentés dans ce projet. Les tests se trouve dans le fichier `compute.test.js` sous le répertoire `test` :

```javascript
// test Compute.add function
test.each([[1, 1, 2], [-1, 2, 1], [-2, -1, -3]])(
    '%i + %i equals %i', (a, b, expected) => {
        expect(operation.add(a, b)).toBe(expected);
    }
);

// test Compute.sub function
test.each([[1, 1, 0], [-1, 1, -2], [5, 3, 2]])(
    '%i - %i equals %i', (a, b, expected) => {
        expect(operation.sub(a, b)).toBe(expected);
    }
);
```

Pour lancer les tests :

```shell
npm test
```

Vous pouvez remarquer que les tests pour l'addition échouent (nous allons corriger ça dans la section suivante).

![](README_assets/test.png)

### Générer la documentation

Pour générer la documentation, nous avons à disposition deux commandes :

* `npm run docs:md` : cette commande va générer la documentation sous forme d'un fichier *Markdown* `doc.md` sous le dossier `documentation` dans la racine du projet
* `npm run docs:html` : cette commande va générer la documentation sous forme d'un site web statique *html* accessible depuis le ficher `index.html` sous le dossier `public/docs`

## Intégration, documentation et déploiement continus

Nous allons mettre en place 3 jobs dans notre **CI / CD pipeline** :

* Un job `test` pour lancer les tests
* Un deuxième job `doc` pour générer la documentation sous forme d'un fichier *Markdown*, et le publier comme objet (artifact)

* Un dernier job `deploy` pour déployer l'application sur un serveur et générer la documentation sous forme *html*

### Créer une branche de développement `dev`

Nous allons commencer par créer une nouvelle branche dans notre projet GitLab appelée `dev` :

```shell
git branch dev
git checkout dev
git push -u origin dev
```

Ces commandes permettent de créer la branche `dev` et la définissent comme la branche actuellement active.

### Créer le fichier de configuration `.gitlab-ci.yml`

Dans la racine du projet, créez un fichier `.gitlab-ci.yml` :

```yaml
image: node:latest

stages:
  - test
  - doc

before_script:
  - npm install

test:
  stage: test
  script:
    - npm test

doc:
  stage: doc
  artifacts:
    paths:
      - documentation/doc.md
  script:
    # Build documentation in md file that will be available from gitlab interface
    - npm run docs:md
```

Avec cette configuration, deux étapes ont été définies : une étape `test` pour lancer les tests, et une étape `doc` pour la génération de la documentation, avec un artifact comme output de l'étape `doc`.

> Vous pouvez trouver des exemples de fichiers `.gitlab-ci.yml` [ici](https://docs.gitlab.com/ee/ci/examples/)

Une fois créé, poussez les changements locaux vers le projet distant :

```shell
git add --all
git commit -m "Add project's CI pipeline configuration"
git push origin dev
```

Maintenant, vous pouvez observer le pipeline gitlab-ci en cours d'exécution sous **CI/CD → Pipelines** dans votre projet GitLab :

![](README_assets/pipeline1.png)

Vous pouvez remarquer que le pipeline échoue à l'étape `test` tel qu'attendu.

Dorénavant, après chaque commit, le pipeline s'exécute, et on peut vérifier s'il réussit ou pas.

Corrigeons le code pour que les tests réussissent.

### Corriger le fonction de l'addition

Le fichier `compute.js` dans la racine du projet, contient les implémentations des opérations de soustraction et d'addition. Dans ce fichier, nous avant laissé sciemment une faute de calcul : signe `*` à la place du signe `+`.

Corrigez cette erreur, et poussez les changement vers le dépôt GitLab distant :

```shell
git add --all
git commit -m "Fix error in addition implementation"
git push origin dev
```

Maintenant, notre pipeline réussit les test, génère et publie la documentation dans un artifact :

![](README_assets/pipeline2.png)

Le bouton à droite vous permet de télécharger les artifacts générés :

![](README_assets/artifact.png)

### Déployer l'application

Jusqu'ici, notre application est disponible seulement en local sur notre machine, en mode développement, sur le port `3000`. Maintenant nous allons déployer notre application sur un serveur de déploiement, une instance **aws** par exemple.

Pour cela, il faut d'abord créer et configurer une instance **aws EC2**. Dans le dossier `aws_script`, nous avons mis à disposition un script en python3 `create_and_run_aws_instance.py` qui permet de créer et lancer une instance **aws** à partir d'une image personnalisée (avec **node** et **npm** installés et une base de données **MongoDB** qui écoute sur le port `27017`).

1. #### Créer des "*credentials*"

* Depuis la [console aws](console.aws.amazon.com), naviguez à **My security credentials** depuis la liste quand vous cliquez sur votre nom d'utilisateur (en haut à droite) :

  ![](README_assets/credentials.png)

* Cliquez sur le menu **Access keys (access key ID and secret access key)**, puis sur le bouton **Create New Access Key** :

![](README_assets/new_key.png)

* Dans la fenêtre qui s'affiche, cliquez sur **Download Key File**, nommez le fichier `rootkey.csv` et enregistrez le dans le dossier `aws_script` dans la racine du projet. 

  > Ce fichier contient vos identifiants (**AWSAccessKeyId** et **AWSSecretKey**), qui vont être consommés par le script python pour créer et lancer une instance aws dans votre compte. Le fichier contenu du fichier `rootkey.csv` dois être sous cette forme :
  >
  > ```
  > AWSAccessKeyId=votre_access_key_id
  > AWSSecretKey=votre_secret_key
  > ```

2. #### Créer et lancer une instance ec2 aws

Dans le dossier `aws_script`, exécutez les commandes suivante :

```shell
# se déplacer dans le dossier où se trouve le script
cd aws_script
# créer un environnement virtuel python3
virtualenv venv
# activer l'environnement virtuel
source venv/bin/activate
# installer les dépendences
pip3 install -r requirements.txt
# lancer le script
python3 create_and_run_aws_instance.py
# désactiver l'environnement virtuel
deactivate
```

Le script `create_and_run_aws_instance.py` permet de :

* Créer une pair de clé pour l’accès ssh à une instance, 
* Enregistrer cette pair de clé dans un fichier nommé `gl_tuto_key.pem`
* Créer un groupe de sécurité avec une règle pour l'accès ssh (port 22) et une règle pour l'application web (port 8080)
* Finalement, créer une instance `gl_tuto_instance` et afficher son adresse ip public affectée

Si le script est lancée pour la première fois, vous aurez le output suivant :

![](README_assets/script_output.png)

Notez l'adresse ip public affecté à cette instance aws, on en aura besoin dans la section suivante.

> Vous pouvez vérifier la création de l’instance dans la console aws :
>
> ![](README_assets/instance_aws.png)

3. #### Ajouter l'étape "déploiement" dans le pipeline ci/cd

Ajoutez le contenu suivant dans le fichier `.gitlab-ci.yml` :

* Dans `stages`, ajoutez une nouvelle étape `deploy`
* A la fin du fichier, ajoutez une tache dans l'étape `deploy` :

```yaml
deploy:
  stage: deploy
  only:
    - master
  # Disable the cache for this job (not needed)
  before_script: []
  script:
    # Any future command that fails will exit the script
    - set -e

    # Install ssh-agent if it's not installed to use scp to copy files into our server
    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'

    # Add the secret key of our deployment server to this gitlab docker image.
    # GitLab at times add spaces which will fail ssh into server
    # and hence the [tr -d '\r']. 
    # Issue here https://gitlab.com/gitlab-examples/ssh-private-key/issues/1
    - eval $(ssh-agent -s)
    - echo "$PRIVATE_KEY" | tr -d '\r' | ssh-add - > /dev/null

    # Below script will disable the prompt we get whenever we ssh into the box 
    # and get the message like this: "The authenticity of the host 'ip address'...
    # To disable this, we need to create a entry in ~/.ssh/config to look something 
    # like this:
    # Host *
    #     StrictHostKeyChecking no
    - mkdir -p ~/.ssh
    - touch ~/.ssh/config
    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" >> ~/.ssh/config

    - echo "deploying to ${SERVER_IP}"

    # Create the .env file with database ip, port and name from gitlab environment variables
    - echo "DATABASE=mongodb://${DB_IP}:${DB_PORT}/${DB_NAME}" >> .env
    - cat .env

    # Copy project files to the deployment server
    - ssh ubuntu@${SERVER_IP} "mkdir -p app"
    - scp -r app.js compute.js models package-lock.json package.json public
        routes tests views .env ubuntu@${SERVER_IP}:app/

    # Restart the project on the deployment server (using forever)
    # https://www.npmjs.com/package/forever
    - ssh ubuntu@${SERVER_IP} "
        forever stopall;
        cd ~/app;
        npm install --only=prod;
        npm run docs:html;
        NODE_ENV=prod forever start app.js
      "
```

La tâche de déploiement fait appel à des variables d'environnement qu'on peut définir directement depuis l'interface web de GitLab. Pour cela, dans la page de votre projet GitLab :

* Naviguez à `Settings`→`CD / DI`.
* Dans la partie `Environment variables `, ajoutez les variables suivantes : 

| Nom variable  | Description                                                  | Valeur                                                       |
| ------------- | :----------------------------------------------------------- | :----------------------------------------------------------- |
| `DB_IP`       | Adresse ip de la base de donnée. Dans notre cas, la base de données s'exécute dans le serveur de déploiement (ce qui est fortement déconseillé dans un environnement de déploiement) | localhost                                                    |
| `DB_NAME`     | Le nom de la base de données                                 | tuto_gl                                                      |
| `DB_PORT`     | Le port sur lequel la base de données écoute                 | 27017                                                        |
| `PRIVATE_KEY` | La clé privé permettant l’accès en ssh à notre serveur de déploiement | Copier-coller le contenu du fichier `gl_tuto_key.pem`        |
| `SERVER_IP`   | L'adresse ip public du serveur de déploiement                | L'adresse IP de l'instance aws créée par le script `create_and_run_aws_instance.py` |

![](README_assets/environment_variables.png)

* Finalement, cliquez sur `Save variables`

Poussez les changement vers le dépôt GitLab distant :

```shell
git add --all
git commit -m "Add deployment stage in ci pipeline"
git push origin dev
```

Notre pipeline réussit les test, génère et publie la documentation dans un artifact, mais n'exécute pas l'étape de déploiement :

![](README_assets/deploy1.png)

Ce comportement est attendu vu que nous avons spécifier que l'étape de déploiement ne s'exécute que pour la branche `master` dans le fichier `.gitlab-ci.ymé` : 

```yaml
deploy:
  stage: deploy
  only:
    - master
```

Maintenant nous allons fusionner la branche "dev" avec la branche "master" :

```shell
# basculement vers la branche master
git checkout master
# fusion de la branche dev dans la branche actuelle (master)
git merge dev
# pousser les modification locale vers le dépôt distant
git push origin master
```

L'étape de déploiement est maintenant effectuée :

![](README_assets/deploy2.png)

Maintenant l’application tourne sur votre serveur de déploiement sur le port 8080 : `SERVER_IP:8080`

![](README_assets/deploy3.png)

> **Remarque:** Si vous voulez pousser des changements sans exécuter le pipeline de GitLab, vous pouvez ajouter `[skip ci]` dans votre message de commit.