23 Feb 2023

Puppet Ci centralization with Gitlab

Sommaire


Introduction

I love pdk

When I used Puppet I’ve discovered the units test and their uses. Working mainly alone, it was very useful for me to set them up with two main objectives:

  • Ensure I don’t do a uncontrolled breaking change
  • Improve the quality of my code in order to contribute to the community code (unit testing tends to revise the way you design your code)

PDK Utilization

Puppetlabs provides a development kit to help set up unit test with : pdk. This tool also allows full context to create your puppet moduls, we will find there the metadata.json, spec directory with helpers, a Gemfile, a hiera context…

This tool is very interesting and easy to use when you have one or two modules to manage, but it becomes much more complicated with 300. To manage and update Pdk, several solutions have been tested. From a simple bash script to a sinatra + sidekiq application, all solutions brought complications. Whether it is to manage the right pdk version, the ruby context or simply push into 300 git repositor at the same time.

After several tests I’ve finally created a solution with Gitlab CI templating. With this operation I already saw several improvements:

  • Update Gitlab CI stages without running a PDK update (with pdk you need to re-define the gitlab-ci on the .sync.yml )
  • Create a self-managed solution for updating PDK
  • Use a gitlab runner and the PDK image provided by Puppetlabs to guarantee it’s operation

Workflow set up

For the desired workflow I defined everything in a dedicated repo and there is only one ci file to include. With this one I will organize the rest, wich gives a result like:

---
default:
  cache:
    paths:
      - vendor/bundle

# Define default variable
variables:
  PDK_TAG: 2.5.0
  IMAGE: pdk:2.5.0.0
  GIT: /opt/puppetlabs/pdk/private/git/bin/git
  PDK: /usr/local/bin/pdk
  LAST_RELEASE: 0.0.1

# Include all my jobs
include:
  - local: /templates/update.yml
  - local: /templates/syntax.yml
  - local: /templates/unittest.yml
  - local: /templates/pages.yml

# Setup my stages
stages:
  - update
  - syntax
  - unittest
  - documentation

Gitlab-ci stages

The different stages are presented in this way (we ignore the stage update for the moment)

Syntax

Set up a syntax lint with rake tasks setup by Pdk, but also a little more checking of yaml files with yamllint.

---
syntax:
  stage: syntax
  image: ruby:2.7.2
  script:
    - bundle exec rake validate lint check rubocop
  rules:
    - when: always

yaml_lint:
  stage: syntax
  image: sdesbure/yamllint
  script:
    - >
        if [[ -f 'hiera.yaml' ]]; then yamllint **/*.yaml --no-warnings; fi
  rules:
    - when: always

Unittest

Launch of unit tests, this task will only be done during merge request or on the production branch. They run with rspec, a framework test for Puppet.

---
.unittest:
  stage: unittest
  image: ruby:2.7.2
  script:
    - bundle exec rake parallel_spec
  only:
    - merge_requests
    - production
  coverage: '/coverage:\s*\d+.\d+%/'

spec_puppet:
  extends: .unittest
  variables:
    PUPPET_GEM_VERSION: '~> 7'

Note that this test is only for Puppet 7, but we can also use parallel-matrix to run it on Puppet 6 and 7.

spec_puppet:
  extends: .unittest
  parallel:
    matrix:
      - PUPPET_GEM_VERSION: ['~> 6', '~> 7']

Pages

And finally the last step used to generate the automatic documentation, with two different formats.

The first will be with Gitlab pages and yard/Puppet strings, but since people will rarely see the pages, we made a simplified markdown version directly in the repo. For this you need a token that has a read/write access to the repo, it will be defined as a variable in the environment variable named PROJECT_TOKEN_RW (of your group containing all your projets or to be done individually projects). The commit will only be made if there is a difference in REFERENCE.md, and in this case the -o ci.skip option will be passed on the push to avoid relaunching the CI.

---
pages:
  stage: documentation
  image: ruby:3.0-alpine
  script:
    - gem install puppet yard puppet-strings
    - puppet strings generate
    - mv doc public
    - apk add git
    - puppet strings generate --format markdown
    - git add REFERENCE.md
    - "[ -z $(git status --porcelain REFERENCE.md) ] && exit 0"
    - "git config --global user.email 'dev@nu.ll'"
    - "git config --global user.name 'Pdk Ci'"
    - "git remote set-url origin https://git:${PROJECT_TOKEN_RW}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
    - "git commit -m ':books: update REFERENCE.md'"
    - "git push -o ci.skip origin HEAD:${CI_COMMIT_BRANCH}"
  artifacts:
    expire_in: 300 seconds
    paths:
      - public
  only:
    - production

Auto-gestion de la CI

All of this is relatively easy to set up and you just need an include like this for it to work:

---
include:
  - project: <votre repo>
    ref: main  # Or your default branch name
    file: templates/pdk-ci.yml

But the main goal was to put a simplified management of PDK. For that I added a check use to notify if a pdk update is required, and I automated the update (and installation) on the CI.

At first, I created a file template/release.yaml who simply contains a variable RELEASE, and in the CI I included the template with a tag and not on the main branch.

template/release.yaml:

---
variables:
  RELEASE: 0.0.1

Then in the file templates/update.yml I’ve:

---
.pdk_update:
  stage: update
  image:
    name: puppet/${IMAGE}
    entrypoint:
      - ''
  script:
    - if [[ ${PDK_RUN} -eq 0 ]]; then exit 0; fi
    - ${GIT} clone --branch ${RELEASE} ${PDK_TEMPLATE} /tmp/pdk_template
    - mkdir -p manifests
    - cp /tmp/pdk_template/configs/sync.yml .sync.yml
    - mkdir -p ~/.pdk/cache/
    - cp /tmp/pdk_template/configs/answers.json ~/.pdk/cache/
    - ${PDK} ${PDK_COMMAND}
    - cp /tmp/pdk_template/configs/gitlab-ci.yml .gitlab-ci.yml
    - touch pdk.yaml
    - ${GIT} add .gitignore
    - ${GIT} config advice.addIgnoredFile false
    - "${GIT} add -A . || :"
    - "${GIT} config --global user.email 'dev@nu.ll'"
    - "${GIT} config --global user.name 'Pdk Ci'"
    - "${GIT} remote set-url origin https://git:${PROJECT_TOKEN_RW}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git"
    - "${GIT} commit -m \"${GIT_COMMIT}\""
    - ${GIT} push origin HEAD:${CI_COMMIT_BRANCH}

install_pdk:
  extends: .pdk_update
  before_script:
    - PDK_RUN=0
    - >
        [[ ! -f '.sync.yml' ]] && PDK_RUN=1
    - export PDK_RUN=${PDK_RUN}
  variables:
    PDK_COMMAND: "convert --force"
    GIT_COMMIT: ':construction_worker: convert to pdk module'
  only:
    - production

update_pdk:
  extends: .pdk_update
  before_script:
    - PDK_RUN=0
    - >
        [[ -f '.sync.yml' ]]
    - >
        [[ -f '.gitlab-ci.yml' ]]
    - >
        [[ "${LAST_RELEASE}" == "${RELEASE}" ]] || PDK_RUN=1
    - export PDK_RUN=${PDK_RUN}
    - export RELEASE=${LAST_RELEASE}
  variables:
    PDK_COMMAND: "update --force --template-ref=${PDK_TAG}"
    GIT_COMMIT: ':construction_worker: upadte pdk'
  only:
    - production

With this comparison [[ "${LAST_RELEASE}" == "${RELEASE}" ]] || PDK_RUN=1 I notify the PDK update, I also check if the file .sync.yml doesn’t exist to run a Pdk installation. To create a new release it will be necessary to go through these 4 steps:

  1. Update the tag used in the new gitlab-ci; this tag will have to be copied (and stocked on configs/gitlab-ci.yml)
  2. Update the RELEASE variable in templates/release.yml
  3. Update the LAST_RELEASE variable in templates/pdk-ci.yml
  4. Create a new tag and push it all

After, your final .gitlab-ci.yaml in your puppet module looks like:

---
include:
  - project: <votre repo>
    ref: main  # Or your default branch name
    file: templates/pdk-ci.yml
  - project: <votre repo>
    ref: <your actual tag>
    file: templates/release.yml

To finish, if you don’t want to wait for your code to be changed on the production to update Pdk, you can use pipeline schedule (for my part I put it randomly once a week when creating the repo).

WARNING: In case your repo containing the templates changes, it’s very important to keep the current repo the time to make a new release with the right references in the gitlab-ci.yaml.


Tags: