From 17f6adb2c35418a2b29ba63f29247509da0520ae Mon Sep 17 00:00:00 2001 From: kjeld Schouten-Lebbing Date: Thu, 4 Feb 2021 15:12:58 +0100 Subject: [PATCH] Add Chart Release System --- .github/ct.yaml | 3 + .github/workflows/chart-testing.yml | 103 ++++++++++++++++++ .github/workflows/charts-release.yaml | 133 ++++++++++++++++++++++++ .github/workflows/deploy_charts.yml | 30 ------ .github/workflows/format_validation.yml | 17 --- .test/charts/common-test_spec.rb | 101 ++++++++++++++++++ .test/test_helper.rb | 119 +++++++++++++++++++++ Gemfile | 12 +++ 8 files changed, 471 insertions(+), 47 deletions(-) create mode 100644 .github/ct.yaml create mode 100644 .github/workflows/chart-testing.yml create mode 100644 .github/workflows/charts-release.yaml delete mode 100644 .github/workflows/deploy_charts.yml delete mode 100644 .github/workflows/format_validation.yml create mode 100644 .test/charts/common-test_spec.rb create mode 100644 .test/test_helper.rb create mode 100644 Gemfile diff --git a/.github/ct.yaml b/.github/ct.yaml new file mode 100644 index 00000000000..3294ec08df7 --- /dev/null +++ b/.github/ct.yaml @@ -0,0 +1,3 @@ +remote: origin +target-branch: master +helm-extra-args: --timeout 600s diff --git a/.github/workflows/chart-testing.yml b/.github/workflows/chart-testing.yml new file mode 100644 index 00000000000..3af8f0620a7 --- /dev/null +++ b/.github/workflows/chart-testing.yml @@ -0,0 +1,103 @@ +name: "Charts: Tests" + +on: + pull_request: + branches: + - '**' + tags-ignore: + - '**' + +jobs: + catalog-tests: + runs-on: ubuntu-latest + container: + image: ixsystems/catalog_validation:latest + + steps: + - uses: actions/checkout@v1 + name: Checkout + - name: Validate catalog format + run: | + /bin/bash -c "PWD=${pwd}; /usr/local/bin/catalog_validate validate --path $PWD" + + + common-lint: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install Helm + uses: azure/setup-helm@v1 + with: + version: v3.4.0 + - uses: actions/setup-python@v2 + with: + python-version: 3.7 + - name: Set up chart-testing + uses: helm/chart-testing-action@v2.0.1 + - name: Run chart-testing (lint) + id: lint + run: ct lint --config .github/ct.yaml --charts 'library/common' + - name: Create kind cluster + uses: helm/kind-action@v1.1.0 + + common-unittest: + runs-on: ubuntu-latest + needs: common-lint + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Install Dev tools + run: sudo apt-get update && sudo apt-get install -y jq libjq-dev + + - name: Install Helm + uses: azure/setup-helm@v1 + with: + version: v3.4.0 + + - name: Install Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 2.7 + + - name: Install dependencies + run: | + export RUBYJQ_USE_SYSTEM_LIBRARIES=1 + bundle install + - name: Run tests + run: | + bundle exec m -r .test/charts + + + chart-tests: + needs: [common-lint, common-unittest, catalog-tests] + runs-on: ubuntu-20.04 + + steps: + - name: Install Helm + run: /bin/bash -c "curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash" + + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Fetch base branch history + run: git fetch origin master:master + + - name: Setup catalog validation + run: | + sudo apt update > /dev/null 2>&1 + sudo apt install -y python3-all-dev python3-pip python3-setuptools > /dev/null 2>&1 + git clone https://github.com/truenas/catalog_validation + sudo pip3 install --disable-pip-version-check --exists-action w -r catalog_validation/requirements.txt > /dev/null 2>&1 + sudo pip3 install -U catalog_validation/. + + - name: Validate changed charts + run: /bin/bash -c "PWD=${pwd}; sudo /usr/local/bin/charts_validate deploy --path $PWD" \ No newline at end of file diff --git a/.github/workflows/charts-release.yaml b/.github/workflows/charts-release.yaml new file mode 100644 index 00000000000..512a92369d4 --- /dev/null +++ b/.github/workflows/charts-release.yaml @@ -0,0 +1,133 @@ +name: "Charts: Release" + +on: + push: + branches: + - master + tags-ignore: + - '**' + paths: + - 'charts/**' + - '!charts/**/README.md' + - 'library/**' + - '!library/**/README.md' + +jobs: + copy: + runs-on: ubuntu-latest + steps: + - name: Checkout-Master + uses: actions/checkout@v2 + with: + ref: 'master' + path: 'master' + - name: Checkout-Charts + uses: actions/checkout@v2 + with: + ref: 'charts' + path: 'charts' + + - name: Generate Helm Structure + run: | + cd master + rm -Rf ../charts/charts/* + for chart in charts/*; do + if [ -d "${chart}" ]; then + maxversion=$(ls -l ${chart} | grep ^d | awk '{print $9}' | tail -n 1) + chartname=$(basename ${chart}) + echo "Processing ${chart} version ${maxversion}" + mv ${chart}/${maxversion} ../charts/charts/${chartname} + fi + done + mv library/* ../charts/charts/ + ls ../charts/charts/ + cd .. + + - name: Commit and push updated charts + run: | + cd charts + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + git add --all + git commit -sm "Publish Chart updates" || exit 0 + git push + + pre-release: + needs: copy + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Block concurrent jobs + uses: softprops/turnstyle@v1 + with: + continue-after-seconds: 180 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + release: + needs: pre-release + runs-on: ubuntu-latest + steps: + - name: Block concurrent jobs + uses: softprops/turnstyle@v1 + with: + continue-after-seconds: 180 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v2 + with: + ref: 'charts' + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - name: Install Helm + uses: azure/setup-helm@v1 + with: + version: v3.4.0 + + - name: Run chart-releaser + uses: helm/chart-releaser-action@v1.1.0 + with: + charts_repo_url: https://charts.truecharts.org/ + env: + CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" + + # Update the generated timestamp in the index.yaml + # needed until https://github.com/helm/chart-releaser/issues/90 + # or helm/chart-releaser-action supports this + post-release: + needs: release + runs-on: ubuntu-latest + steps: + - name: Block concurrent jobs + uses: softprops/turnstyle@v1 + with: + continue-after-seconds: 180 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Checkout + uses: actions/checkout@v2 + with: + ref: "gh-pages" + fetch-depth: 0 + + - name: Configure Git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + - name: Commit and push timestamp updates + run: | + if [[ -f index.yaml ]]; then + git pull + export generated_date=$(date --utc +%FT%T.%9NZ) + sed -i -e "s/^generated:.*/generated: \"$generated_date\"/" index.yaml + git add index.yaml + git commit -sm "Update generated timestamp [ci-skip]" || exit 0 + git push + fi diff --git a/.github/workflows/deploy_charts.yml b/.github/workflows/deploy_charts.yml deleted file mode 100644 index 36ee3b587b9..00000000000 --- a/.github/workflows/deploy_charts.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Charts-CI - -on: [push, pull_request] - -jobs: - deploy-charts: - runs-on: ubuntu-20.04 - - steps: - - name: Install Helm - run: /bin/bash -c "curl https://raw.githubusercontent.com/helm/helm/master/scripts/get-helm-3 | bash" - - - name: Checkout - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - - name: Fetch base branch history - run: git fetch origin master:master - - - name: Setup catalog validation - run: | - sudo apt update > /dev/null 2>&1 - sudo apt install -y python3-all-dev python3-pip python3-setuptools > /dev/null 2>&1 - git clone https://github.com/truenas/catalog_validation - sudo pip3 install --disable-pip-version-check --exists-action w -r catalog_validation/requirements.txt > /dev/null 2>&1 - sudo pip3 install -U catalog_validation/. - - - name: Validate changed charts - run: /bin/bash -c "PWD=${pwd}; sudo /usr/local/bin/charts_validate deploy --path $PWD" diff --git a/.github/workflows/format_validation.yml b/.github/workflows/format_validation.yml deleted file mode 100644 index 5360810ff5b..00000000000 --- a/.github/workflows/format_validation.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: format_validation - -on: [push, pull_request] - -jobs: - build: - - runs-on: ubuntu-latest - container: - image: ixsystems/catalog_validation:latest - - steps: - - uses: actions/checkout@v1 - name: Checkout - - name: Validate catalog format - run: | - /bin/bash -c "PWD=${pwd}; /usr/local/bin/catalog_validate validate --path $PWD" diff --git a/.test/charts/common-test_spec.rb b/.test/charts/common-test_spec.rb new file mode 100644 index 00000000000..5d68adfeaad --- /dev/null +++ b/.test/charts/common-test_spec.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true +require_relative '../test_helper' + +class Test < ChartTest + @@chart = Chart.new('library/common-test') + + describe @@chart.name do + describe 'controller type' do + it 'defaults to "Deployment"' do + assert_nil(resource('StatefulSet')) + assert_nil(resource('DaemonSet')) + refute_nil(resource('Deployment')) + end + + it 'accepts "statefulset"' do + chart.value controllerType: 'statefulset' + assert_nil(resource('Deployment')) + assert_nil(resource('DaemonSet')) + refute_nil(resource('StatefulSet')) + end + + it 'accepts "daemonset"' do + chart.value controllerType: 'daemonset' + assert_nil(resource('Deployment')) + assert_nil(resource('StatefulSet')) + refute_nil(resource('DaemonSet')) + end + end + + describe 'pod replicas' do + it 'defaults to 1' do + jq('.spec.replicas', resource('Deployment')).must_equal 1 + end + + it 'accepts integer as value' do + chart.value replicas: 3 + jq('.spec.replicas', resource('Deployment')).must_equal 3 + end + end + + describe 'ports settings' do + default_name = 'http' + default_port = 8080 + + it 'defaults to name "http" on port 8080' do + jq('.spec.ports[0].port', resource('Service')).must_equal default_port + jq('.spec.ports[0].targetPort', resource('Service')).must_equal default_name + jq('.spec.ports[0].name', resource('Service')).must_equal default_name + jq('.spec.template.spec.containers[0].ports[0].containerPort', resource('Deployment')).must_equal default_port + jq('.spec.template.spec.containers[0].ports[0].name', resource('Deployment')).must_equal default_name + end + + it 'port name can be overridden' do + values = { + service: { + port: { + name: 'server' + } + } + } + chart.value values + jq('.spec.ports[0].port', resource('Service')).must_equal default_port + jq('.spec.ports[0].targetPort', resource('Service')).must_equal values[:service][:port][:name] + jq('.spec.ports[0].name', resource('Service')).must_equal values[:service][:port][:name] + jq('.spec.template.spec.containers[0].ports[0].containerPort', resource('Deployment')).must_equal default_port + jq('.spec.template.spec.containers[0].ports[0].name', resource('Deployment')).must_equal values[:service][:port][:name] + end + + it 'targetPort can be overridden' do + values = { + service: { + port: { + targetPort: 80 + } + } + } + chart.value values + jq('.spec.ports[0].port', resource('Service')).must_equal default_port + jq('.spec.ports[0].targetPort', resource('Service')).must_equal values[:service][:port][:targetPort] + jq('.spec.ports[0].name', resource('Service')).must_equal default_name + jq('.spec.template.spec.containers[0].ports[0].containerPort', resource('Deployment')).must_equal values[:service][:port][:targetPort] + jq('.spec.template.spec.containers[0].ports[0].name', resource('Deployment')).must_equal default_name + end + + it 'targetPort cannot be a named port' do + values = { + service: { + port: { + targetPort: 'test' + } + } + } + chart.value values + exception = assert_raises HelmCompileError do + chart.execute_helm_template! + end + assert_match("Our charts do not support named ports for targetPort. (port name #{default_name}, targetPort #{values[:service][:port][:targetPort]})", exception.message) + end + end + end +end diff --git a/.test/test_helper.rb b/.test/test_helper.rb new file mode 100644 index 00000000000..2deecf2f3bc --- /dev/null +++ b/.test/test_helper.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'json' +require 'yaml' +require 'open3' + +require 'jq/extend' +require 'minitest-implicit-subject' +require "minitest/reporters" +require 'minitest/autorun' +require 'minitest/pride' + +class HelmCompileError < StandardError +end + +class HelmDepsError < StandardError +end + +class Chart + attr_reader :name, :path, :values + + def initialize(chart) + @name = chart.split('/').last + + @path = File.expand_path(chart) + + @values = default_values + + update_deps! + end + + def update_deps! + command = "helm dep update '#{path}'" + stdout, stderr, status = Open3.capture3(command) + raise HelmDepsError, stderr if status != 0 + end + + def reset! + @values = default_values + @parsed_resources = nil + end + + def value(value) + values.merge!(value) + end + + def configure_custom_name(name) + @name = name + end + + def execute_helm_template! + file = Tempfile.new(name) + file.write(JSON.parse(values.to_json).to_yaml) + file.close + + begin + command = "helm template '#{name}' '#{path}' --namespace='default' --values='#{file.path}'" + stdout, stderr, status = Open3.capture3(command) + + raise HelmCompileError, stderr if status != 0 + + stdout + ensure + file.unlink + end + end + + def parsed_resources + @parsed_resources ||= begin + output = execute_helm_template! + puts output if ENV.fetch('DEBUG', 'false') == 'true' + YAML.load_stream(output) + end + end + + def resources(matcher = nil) + return parsed_resources unless matcher + + parsed_resources.select do |r| + r >= Hash[matcher.map { |k, v| [k.to_s, v] }] + end + end + + def default_values + { + } + end +end + +class ExtendedMinitest < Minitest::Test + extend MiniTest::Spec::DSL +end + +class ChartTest < ExtendedMinitest + Minitest::Reporters.use! Minitest::Reporters::SpecReporter.new + + before do + chart.reset! + end + + def chart + self.class.class_variable_get('@@chart') + end + + def resource(name) + chart.resources(kind: name).first + end + + def jq(matcher, object) + value(object.jq(matcher)[0]) + end +end + +class Minitest::Result + def name + test_name = defined?(@name) ? @name : super + test_name.to_s.gsub /\Atest_\d{4,}_/, "" + end +end diff --git a/Gemfile b/Gemfile new file mode 100644 index 00000000000..ac5b8e7319a --- /dev/null +++ b/Gemfile @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +group :test do + gem 'm' + gem 'minitest' + gem 'minitest-implicit-subject' + gem 'minitest-reporters' + gem 'pry' + gem 'ruby-jq' +end