From 958d783919799e5851754b244d8094b9b7c82054 Mon Sep 17 00:00:00 2001
From: Matt McEuen <madgin@madgin.net>
Date: Wed, 27 May 2020 15:43:34 -0500
Subject: [PATCH] Add site doc validation gate

This adds a gate which loops over all phases in all sites,
and performs an airshipctl apply --dry-run on them to ensure YAML
validity and schema adherence.  Aside from installation tasks,
the gate is run via a makefile entrypoint so that it can be
easily consumed by developers or by non-zuul CICD platforms.

Change-Id: Ie4ab246848a580ab20c3153af1e3749a27e3f770
---
 Makefile                                      |  11 ++
 ...airship-airshipctl-validate-documents.yaml |  28 ++++
 tools/document/build_kustomize_plugin.sh      |  46 +++++++
 tools/document/get_kind.sh                    |  25 ++++
 tools/document/start_kind.sh                  |  27 ++++
 tools/document/validate_site_docs.sh          | 127 ++++++++++++++++++
 tools/validate_docs                           |  39 ++++++
 zuul.d/jobs.yaml                              |   7 +
 zuul.d/projects.yaml                          |   2 +
 9 files changed, 312 insertions(+)
 create mode 100644 playbooks/airship-airshipctl-validate-documents.yaml
 create mode 100755 tools/document/build_kustomize_plugin.sh
 create mode 100755 tools/document/get_kind.sh
 create mode 100755 tools/document/start_kind.sh
 create mode 100755 tools/document/validate_site_docs.sh
 create mode 100755 tools/validate_docs

diff --git a/Makefile b/Makefile
index fd969df70..c30513d7d 100644
--- a/Makefile
+++ b/Makefile
@@ -45,6 +45,12 @@ GD_PORT             ?= 8080
 # Documentation location
 DOCS_DIR            ?= docs
 
+# document validation options
+UNAME               != uname
+export KIND_URL     ?= https://kind.sigs.k8s.io/dl/v0.8.1/kind-$(UNAME)-amd64
+KUBECTL_VERSION     ?= v1.16.2
+export KUBECTL_URL  ?= https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl
+
 .PHONY: depend
 depend:
 	@go mod download
@@ -206,3 +212,8 @@ add-copyright:
 .PHONY: check-copyright
 check-copyright:
 	@./tools/check_copyright
+
+# Validate YAMLs for all sites
+.PHONY: validate-docs
+validate-docs:
+	@./tools/validate_docs
diff --git a/playbooks/airship-airshipctl-validate-documents.yaml b/playbooks/airship-airshipctl-validate-documents.yaml
new file mode 100644
index 000000000..9948a0f28
--- /dev/null
+++ b/playbooks/airship-airshipctl-validate-documents.yaml
@@ -0,0 +1,28 @@
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+- hosts: primary
+  tasks:
+
+    - name: Ensure kubectl is present
+      include_role:
+        name: install-kubectl
+
+    - name: Ensure airshipctl is present
+      include_role:
+        name: airshipctl-systemwide-executable
+
+    - name: Validating site documents
+      make:
+        target: validate-docs
+        chdir: "{{ zuul.project.src_dir }}"
+
diff --git a/tools/document/build_kustomize_plugin.sh b/tools/document/build_kustomize_plugin.sh
new file mode 100755
index 000000000..de8a4787d
--- /dev/null
+++ b/tools/document/build_kustomize_plugin.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Run this from the airshipctl project root
+set -e
+
+# This script builds a version of kustomize that is able to acces
+# the ReplacementTransformer plugin.
+# It assumes a build airshipctl binary in bin/, or if not,
+# somewhere on the path.
+if [ -f "bin/airshipctl" ]; then
+    AIRSHIPCTL="bin/airshipctl"
+else
+    AIRSHIPCTL="$(which airshipctl)"
+fi
+
+: ${KUSTOMIZE_PLUGIN_HOME:="$HOME/.airship/kustomize-plugins"}
+: ${AIRSHIP_TAG:="dev"}
+
+# purge any previous airship plugins
+rm -rf ${KUSTOMIZE_PLUGIN_HOME}/airshipit.org
+
+# copy our plugin to the PLUGIN_ROOT, and give a kustomzie-friendly wrapper
+PLUGIN_PATH=${KUSTOMIZE_PLUGIN_HOME}/airshipit.org/v1alpha1/replacementtransformer
+mkdir -p ${PLUGIN_PATH}
+cat > ${PLUGIN_PATH}/ReplacementTransformer <<EOF
+#!/bin/bash
+\$(dirname \$0)/airshipctl document plugin "\$@"
+EOF
+chmod +x ${PLUGIN_PATH}/ReplacementTransformer
+cp -p ${AIRSHIPCTL} ${PLUGIN_PATH}/
+
+# tell the user how to use this
+echo -e "The airshipctl kustomize plugin has been installed.\nRun kustomize with:"
+echo -e "KUSTOMIZE_PLUGIN_HOME=$KUSTOMIZE_PLUGIN_HOME \$GOPATH/bin/kustomize build --enable_alpha_plugins ..."
diff --git a/tools/document/get_kind.sh b/tools/document/get_kind.sh
new file mode 100755
index 000000000..7d7f4b696
--- /dev/null
+++ b/tools/document/get_kind.sh
@@ -0,0 +1,25 @@
+#!/bin/bash
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This downloads kind, puts it in a temp directory, and prints the directory
+set -e
+
+: ${KIND_URL:="https://kind.sigs.k8s.io/dl/v0.8.1/kind-$(uname)-amd64"}
+TMP=$(mktemp -d)
+KIND="${TMP}/kind"
+
+curl -sSLo ${KIND} ${KIND_URL}
+chmod +x ${KIND}
+
+echo ${TMP}
diff --git a/tools/document/start_kind.sh b/tools/document/start_kind.sh
new file mode 100755
index 000000000..8e6cef5bd
--- /dev/null
+++ b/tools/document/start_kind.sh
@@ -0,0 +1,27 @@
+#!/bin/bash
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# This starts up a kubernetes cluster which is sufficient for
+# assisting with tasks like `kubectl apply --dry-run` style validation
+set -e
+
+: ${KIND:="/usr/local/bin/kind"}
+: ${CLUSTER:="airship"} # NB: kind prepends "kind-"
+: ${KUBECONFIG:="${HOME}/.airship/kubeconfig"}
+
+${KIND} create cluster --name ${CLUSTER} \
+    --kubeconfig ${KUBECONFIG}
+
+echo "This cluster can be deleted via:"
+echo "${KIND} delete cluster --name ${CLUSTER}"
diff --git a/tools/document/validate_site_docs.sh b/tools/document/validate_site_docs.sh
new file mode 100755
index 000000000..717e7011e
--- /dev/null
+++ b/tools/document/validate_site_docs.sh
@@ -0,0 +1,127 @@
+#!/bin/bash
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -e
+
+: ${PROJECT_ROOT:=${PWD}}
+: ${SITE:="test-workload"}
+: ${CONTEXT:="kind-airship"}
+: ${KUBECONFIG:="${HOME}/.airship/kubeconfig"}
+
+: ${KUBECTL:="/usr/local/bin/kubectl"}
+: ${KUSTOMIZE_PLUGIN_HOME:="${HOME}/.airship/kustomize-plugins"}
+TMP=$(mktemp -d)
+
+# Use the local project airshipctl binary as the default if it exists,
+# otherwise use the one on the PATH
+if [ -f "bin/airshipctl" ]; then
+    AIRSHIPCTL_DEFAULT="bin/airshipctl"
+else
+    AIRSHIPCTL_DEFAULT="$(which airshipctl)"
+fi
+
+: ${AIRSHIPCONFIG:="${TMP}/config"}
+: ${AIRSHIPKUBECONFIG:="${TMP}/kubeconfig"}
+: ${AIRSHIPCTL:="${AIRSHIPCTL_DEFAULT}"}
+ACTL="${AIRSHIPCTL} --airshipconf ${AIRSHIPCONFIG} --kubeconfig ${AIRSHIPKUBECONFIG}"
+
+export KUSTOMIZE_PLUGIN_HOME
+export KUBECONFIG
+
+# TODO: use `airshipctl config` to do this once all the needed knobs are exposed
+# The non-default parts are to set the targetPath and subPath appropriately,
+# and to craft up cluster/contexts to avoid the need for automatic kubectl reconciliation
+function generate_airshipconf {
+    cluster=$1
+
+    cat <<EOL > ${AIRSHIPCONFIG}
+apiVersion: airshipit.org/v1alpha1
+bootstrapInfo:
+  default:
+    builder:
+      networkConfigFileName: network-config
+      outputMetadataFileName: output-metadata.yaml
+      userDataFileName: user-data
+    container:
+      containerRuntime: docker
+      image: quay.io/airshipit/isogen:latest-debian_stable
+      volume: /srv/iso:/config
+    remoteDirect:
+      isoUrl: http://localhost:8099/debian-custom.iso
+clusters:
+  ${CONTEXT}_${cluster}:
+    clusterType:
+      ${cluster}:
+        bootstrapInfo: default
+        clusterKubeconf: ${CONTEXT}_${cluster}
+        managementConfiguration: default
+contexts:
+  ${CONTEXT}_${cluster}:
+    contextKubeconf: ${CONTEXT}_${cluster}
+    manifest: ${CONTEXT}_${cluster}
+currentContext: ${CONTEXT}_${cluster}
+kind: Config
+managementConfiguration:
+  default:
+    insecure: true
+    systemActionRetries: 30
+    systemRebootDelay: 30
+    type: redfish
+manifests:
+  ${CONTEXT}_${cluster}:
+    primaryRepositoryName: primary
+    repositories:
+      primary:
+        checkout:
+          branch: master
+          commitHash: ""
+          force: false
+          tag: ""
+        url: https://opendev.org/airship/treasuremap
+    subPath: manifests/site/${SITE}
+    targetPath: .
+users:
+  ${CONTEXT}_${cluster}: {}
+EOL
+}
+
+# Loop over all cluster types and phases for the given site
+for cluster in ephemeral target; do
+    # Clear out any CRDs left from testing of a previous cluster
+    ${KUBECTL} --context ${CONTEXT} --kubeconfig ${KUBECONFIG} delete crd --all > /dev/null
+
+    if [[ -d "manifests/site/${SITE}/${cluster}" ]]; then
+        # Since we'll be mucking with the kubeconfig - make a copy of it and muck with the copy
+        cp ${KUBECONFIG} ${AIRSHIPKUBECONFIG}
+        # This is a big hack to work around kubeconfig reconciliation
+        # change the cluster name (as well as context and user) to avoid kubeconfig reconciliation
+        sed -i "s/${CONTEXT}/${CONTEXT}_${cluster}/" ${AIRSHIPKUBECONFIG}
+        generate_airshipconf ${cluster}
+
+        for phase in $(ls manifests/site/${SITE}/${cluster}| grep -v "\.yaml$"); do
+            echo -e "\n*** Rendering ${cluster}/${phase}"
+
+            # step 1: actually apply all crds in the phase
+            # TODO: will need to loop through phases in order, eventually
+            # e.g., load CRDs from initinfra first, so they're present when validating later phases
+            ${ACTL} phase render ${phase} -k CustomResourceDefinition > ${TMP}/crds.yaml
+            if [ -s ${TMP}/crds.yaml ]; then
+                ${KUBECTL} --context ${CONTEXT} --kubeconfig ${KUBECONFIG} apply -f ${TMP}/crds.yaml
+            fi
+
+            # step 2: dry-run the entire phase
+            ${ACTL} phase apply --dry-run ${phase}
+        done
+    fi
+done
diff --git a/tools/validate_docs b/tools/validate_docs
new file mode 100755
index 000000000..1bddde295
--- /dev/null
+++ b/tools/validate_docs
@@ -0,0 +1,39 @@
+#!/usr/bin/env bash
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# The makefile entrypoint driver for document validation
+# Expected to be run from the project root
+set -e
+
+# get kind
+echo "Fetching kind from ${KIND_URL}..."
+TMP=$(KIND_URL=${KIND_URL} ./tools/document/get_kind.sh)
+export KIND=${TMP}/kind
+export KUBECTL_URL
+
+function cleanup() {
+    ${KIND} delete cluster --name airship
+    rm -rf ${TMP}
+}
+trap cleanup EXIT
+
+./tools/document/build_kustomize_plugin.sh
+./tools/document/start_kind.sh
+
+for site in $(ls manifests/site); do
+    echo -e "\nValidating site: ${site}\n****************"
+    SITE=${site} ./tools/document/validate_site_docs.sh
+    echo "Validation of site ${site} is succesful!"
+done
+
diff --git a/zuul.d/jobs.yaml b/zuul.d/jobs.yaml
index 0a4ba07df..2da77cdc6 100644
--- a/zuul.d/jobs.yaml
+++ b/zuul.d/jobs.yaml
@@ -45,6 +45,13 @@
       - ^.*\.md$
       - ^docs/.*$
 
+- job:
+    name: airship-airshipctl-validate-site-docs
+    pre-run:
+      - playbooks/airship-airshipctl-deploy-docker.yaml
+    run: playbooks/airship-airshipctl-validate-documents.yaml
+    nodeset: airship-airshipctl-single-node
+
 - job:
     name: airship-airshipctl-functional-existing-k8s
     pre-run: playbooks/airship-airshipctl-deploy-existing-k8s.yaml
diff --git a/zuul.d/projects.yaml b/zuul.d/projects.yaml
index a865c1734..629882342 100644
--- a/zuul.d/projects.yaml
+++ b/zuul.d/projects.yaml
@@ -22,6 +22,7 @@
         - airship-airshipctl-lint-unit
         - airship-airshipctl-golint
         - airship-airshipctl-build-image
+        - airship-airshipctl-validate-site-docs
 #        - airship-airshipctl-functional-existing-k8s TODO: Enable this when functional tests exist, and a cluster is up
         - airship-airshipctl-gate-test
         - airship-airshipctl-32GB-gate-test
@@ -31,6 +32,7 @@
         - airship-airshipctl-lint-unit
         - airship-airshipctl-lint-unit
         - airship-airshipctl-build-image
+        - airship-airshipctl-validate-site-docs
 #        - airship-airshipctl-functional-existing-k8s TODO: Enable this when functional tests exist, and a cluster is up
         - airship-airshipctl-gate-test
     post: