From f5c32f71f4a65c70745c610b1c9c096f4391c79b Mon Sep 17 00:00:00 2001
From: gabriel klawitter <gabreal@users.noreply.github.com>
Date: Wed, 27 Feb 2019 12:37:10 +0100
Subject: [PATCH] ci: add kubernetes helm chart and gcp deployment (#1854)

* ci: add kubernetes helm chart and gcp deployment

* use official or parity's docker images only
---
 substrate/.gitlab-ci.yml                      | 122 ++++++++++++----
 .../scripts/gitlab/check_merge_conflict.sh    |   6 +-
 substrate/scripts/gitlab/check_runtime.sh     |  19 ++-
 substrate/scripts/kubernetes/Chart.yaml       |  12 ++
 substrate/scripts/kubernetes/README.md        |  47 ++++++
 .../templates/poddisruptionbudget.yaml        |  10 ++
 .../scripts/kubernetes/templates/secrets.yaml |  11 ++
 .../scripts/kubernetes/templates/service.yaml |  39 +++++
 .../kubernetes/templates/serviceaccount.yaml  |  10 ++
 .../kubernetes/templates/statefulset.yaml     | 135 ++++++++++++++++++
 substrate/scripts/kubernetes/values.yaml      |  53 +++++++
 11 files changed, 426 insertions(+), 38 deletions(-)
 create mode 100644 substrate/scripts/kubernetes/Chart.yaml
 create mode 100644 substrate/scripts/kubernetes/README.md
 create mode 100644 substrate/scripts/kubernetes/templates/poddisruptionbudget.yaml
 create mode 100644 substrate/scripts/kubernetes/templates/secrets.yaml
 create mode 100644 substrate/scripts/kubernetes/templates/service.yaml
 create mode 100644 substrate/scripts/kubernetes/templates/serviceaccount.yaml
 create mode 100644 substrate/scripts/kubernetes/templates/statefulset.yaml
 create mode 100644 substrate/scripts/kubernetes/values.yaml

diff --git a/substrate/.gitlab-ci.yml b/substrate/.gitlab-ci.yml
index bda6a6e5e8f..aa74056d3ec 100644
--- a/substrate/.gitlab-ci.yml
+++ b/substrate/.gitlab-ci.yml
@@ -7,7 +7,7 @@ stages:
   - test
   - build
   - publish
-  - label
+  - deploy
 
 image:                             parity/rust:nightly
 
@@ -20,11 +20,7 @@ variables:
 
 
 
-cache:
-  key:                             "${CI_JOB_NAME}"
-  paths:
-    - ${CARGO_HOME}
-    - ./target
+cache:                             {}
 
 .collect_artifacts:                &collect_artifacts
   artifacts:
@@ -36,14 +32,21 @@ cache:
 
 
 
+.kubernetes_build:                 &kubernetes_build
+  tags:
+    - kubernetes-parity-build
+  environment:
+    name: parity-build
+
+
+
 #### stage:                        merge-test
 
 check:merge:conflict:
   stage:                           merge-test
   image:                           parity/tools:latest
   cache:                           {}
-  tags:
-    - linux-docker
+  <<:                              *kubernetes_build
   only:
     - /^[0-9]+$/
   variables:
@@ -62,8 +65,7 @@ check:runtime:
   stage:                           test
   image:                           parity/tools:latest
   cache:                           {}
-  tags:
-    - linux-docker
+  <<:                              *kubernetes_build
   only:
     - /^[0-9]+$/
   variables:
@@ -77,25 +79,29 @@ check:runtime:
 
 test:rust:stable:                  &test
   stage:                           test
+  cache:
+    key:                           "${CI_JOB_NAME}-test"
+    paths:
+      - ${CARGO_HOME}
+      - ./target
   variables:
     RUST_TOOLCHAIN: stable
     # Enable debug assertions since we are running optimized builds for testing
     # but still want to have debug assertions.
     RUSTFLAGS: -Cdebug-assertions=y
     TARGET: native
+  tags:
+    - linux-docker
   only:
     - tags
     - master
     - schedules
     - web
-    - /^pr-[0-9]+$/
     - /^[0-9]+$/
-  tags:
-    - linux-docker
   before_script:
-   - test -d ${CARGO_HOME} -a -d ./target && 
+   - test -d ${CARGO_HOME} -a -d ./target &&
      echo "build cache size:" &&
-     du -hs ${CARGO_HOME} ./target
+     du -h --max-depth=2 ${CARGO_HOME} ./target
    - ./scripts/build.sh
   script:
     - time cargo test --all --release --verbose --locked
@@ -115,6 +121,11 @@ test:rust:stable:                  &test
 
 build:rust:linux:release:          &build
   stage:                           build
+  cache:
+    key:                           "${CI_JOB_NAME}-build"
+    paths:
+      - ${CARGO_HOME}
+      - ./target
   <<:                              *collect_artifacts
   <<:                              *build_only
   tags:
@@ -126,15 +137,17 @@ build:rust:linux:release:          &build
     - mkdir -p ./artifacts
     - mv ./target/release/substrate ./artifacts/.
     - echo -n "Substrate version = "
-    - if [ "${CI_COMMIT_TAG}" ]; then 
+    - if [ "${CI_COMMIT_TAG}" ]; then
         echo "${CI_COMMIT_TAG}" | tee ./artifacts/VERSION;
-      else 
+      else
         ./artifacts/substrate --version |
         sed -n -r 's/^substrate ([0-9.]+.*-[0-9a-f]{7,13})-.*$/\1/p' |
         tee ./artifacts/VERSION;
       fi
     - sha256sum ./artifacts/substrate | tee ./artifacts/substrate.sha256
+    - echo "\n# building node-template\n"
     - ./scripts/node-template-release.sh ./artifacts/substrate-node-template.tar.gz
+    - cp -r scripts/docker/* ./artifacts
 
 
 
@@ -167,16 +180,20 @@ build:rust:doc:release:            &build
     - build:rust:linux:release
   cache: {}
   <<:                              *build_only
+  <<:                              *kubernetes_build
 
 
 
 publish:docker:release:
   <<:                              *publish_build
-  tags:
-    - shell
+  image:                           docker:stable
+  services:
+    - docker:dind
   variables:
+    DOCKER_HOST:                   tcp://localhost:2375
+    DOCKER_DRIVER:                 overlay2
     GIT_STRATEGY:                  none
-    DOCKERFILE:                    scripts/docker/Dockerfile
+    # DOCKERFILE:                  scripts/docker/Dockerfile
     CONTAINER_IMAGE:               parity/substrate
   script:
     - VERSION="$(cat ./artifacts/VERSION)"
@@ -184,7 +201,8 @@ publish:docker:release:
         || ( echo "no docker credentials provided"; exit 1 )
     - docker login -u "$Docker_Hub_User_Parity" -p "$Docker_Hub_Pass_Parity"
     - docker info
-    - docker build --tag $CONTAINER_IMAGE:$VERSION --tag $CONTAINER_IMAGE:latest -f $DOCKERFILE ./artifacts/
+    - cd ./artifacts
+    - docker build --tag $CONTAINER_IMAGE:$VERSION --tag $CONTAINER_IMAGE:latest .
     - docker push $CONTAINER_IMAGE:$VERSION
     - docker push $CONTAINER_IMAGE:latest
   after_script:
@@ -213,8 +231,6 @@ publish:s3:release:
   after_script:
     - aws s3 ls s3://${BUCKET}/${PREFIX}/latest/
         --recursive --human-readable --summarize
-  tags:
-    - linux-docker
 
 
 
@@ -223,11 +239,8 @@ publish:s3:doc:
   dependencies:
     - build:rust:doc:release
   cache: {}
-  only:
-    - master
-    - tags
-    - web
-    - publish-rustdoc
+  <<:                              *build_only
+  <<:                              *kubernetes_build
   variables:
     GIT_STRATEGY:                  none
     BUCKET:                        "releases.parity.io"
@@ -242,10 +255,59 @@ publish:s3:doc:
   after_script:
     - aws s3 ls s3://${BUCKET}/${PREFIX}/
         --human-readable --summarize
-  tags:
-    - linux-docker
 
 
 
 
 
+
+
+.deploy:template:                  &deploy
+  stage:                           deploy
+  when:                            manual
+  cache:                           {}
+  retry:                           1
+  image:                           parity/kubectl-helm:$HELM_VERSION
+  <<:                              *build_only
+  # variables:
+  #   DEPLOY_TAG:                    "latest"
+  tags:
+    - kubernetes-parity-build
+  before_script:
+    - test -z "${DEPLOY_TAG}" &&
+      test -f ./target/release/VERSION &&
+      DEPLOY_TAG="$(cat ./target/release/VERSION)"
+    - test "${DEPLOY_TAG}" || ( echo "Neither DEPLOY_TAG nor VERSION information available"; exit 1 )
+  script:
+    - echo "Substrate version = ${DEPLOY_TAG}"
+    # or use helm to render the template
+    - helm template
+      --values ./scripts/kubernetes/values.yaml
+      --set image.tag=${DEPLOY_TAG}
+      --set validator.keys=${VALIDATOR_KEYS}
+      ./scripts/kubernetes | kubectl apply -f - --dry-run=false
+    - echo "# substrate namespace"
+    - kubectl -n substrate get all
+    - echo "# substrate's nodes' external ip addresses:"
+    - kubectl get nodes -l node=substrate
+      -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{range @.status.addresses[?(@.type=="ExternalIP")]}{.address}{"\n"}{end}'
+    - echo "# substrate' nodes"
+    - kubectl -n substrate get pods
+      -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.nodeName}{"\n"}{end}'
+
+
+
+# have environment:url eventually point to the logs
+
+deploy:ew3:
+  <<:                              *deploy
+  environment:
+    name: parity-prod-ew3
+
+deploy:ue1:
+  <<:                              *deploy
+  environment:
+    name: parity-prod-ue1
+
+
+
diff --git a/substrate/scripts/gitlab/check_merge_conflict.sh b/substrate/scripts/gitlab/check_merge_conflict.sh
index 29ed6906328..dd677ff7620 100755
--- a/substrate/scripts/gitlab/check_merge_conflict.sh
+++ b/substrate/scripts/gitlab/check_merge_conflict.sh
@@ -45,6 +45,11 @@ EOT
 
 test "${mergeable}" = "true" && echo "|  yes, it is." && exit 0
 
+if [ "${baseref}" = "null" -o "${baserepo}" = "null" ]
+then
+	echo "| either connectivity issues with github or pull request not existant"
+	exit 3
+fi
 
 cat <<-EOT
 |  not mergeable
@@ -94,7 +99,6 @@ curl -sS -X POST \
 	-F "token=${CI_JOB_TOKEN}" \
 	-F "ref=master" \
 	-F "variables[REBUILD_WASM]=\"${baserepo}:${baseref}\"" \
-	-F "variables[PRNO]=${CI_COMMIT_REF_NAME}" \
 	${GITLAB_API}/projects/${GITHUB_API_PROJECT}/trigger/pipeline \
 	| jq -r .web_url
 
diff --git a/substrate/scripts/gitlab/check_runtime.sh b/substrate/scripts/gitlab/check_runtime.sh
index 323e38fd19d..61068e7eb88 100755
--- a/substrate/scripts/gitlab/check_runtime.sh
+++ b/substrate/scripts/gitlab/check_runtime.sh
@@ -1,9 +1,9 @@
 #!/bin/sh
-# 
-# 
-# check for any changes in the node/src/runtime, srml/ and core/sr_* trees. if 
-# there are any changes found, it should mark the PR breaksconsensus and 
-# "auto-fail" the PR in some way unless a) the runtime is rebuilt and b) there 
+#
+#
+# check for any changes in the node/src/runtime, srml/ and core/sr_* trees. if
+# there are any changes found, it should mark the PR breaksconsensus and
+# "auto-fail" the PR in some way unless a) the runtime is rebuilt and b) there
 # isn't a change in the runtime/src/lib.rs file that alters the version.
 
 set -e # fail on any error
@@ -66,14 +66,14 @@ then
 	then
 		cat <<-EOT
 			
-			changes to the runtime sources and changes in the spec version. Wasm 
+			changes to the runtime sources and changes in the spec version. Wasm
 			binary blob is rebuilt. Looks good.
 		
 			spec_version: ${sub_spec_version} -> ${add_spec_version}
 		
 		EOT
 		exit 0
-	else 
+	else
 		cat <<-EOT
 			
 			changes to the runtime sources and changes in the spec version. Wasm
@@ -122,6 +122,11 @@ else
 
 	versions file: ${VERSIONS_FILE}
 
+	note: if the master branch was merged in as automated wasm rebuilds do it
+	might be the case that a {spec,impl}_version has been changed. but for pull
+	requests that involve wasm source file changes a version has to be changed
+	in the pull request itself.
+
 	EOT
 
 	# drop through into pushing `gotissues` and exit 1...
diff --git a/substrate/scripts/kubernetes/Chart.yaml b/substrate/scripts/kubernetes/Chart.yaml
new file mode 100644
index 00000000000..4dd133c860b
--- /dev/null
+++ b/substrate/scripts/kubernetes/Chart.yaml
@@ -0,0 +1,12 @@
+name: substrate
+version: 0.1
+appVersion: 0.9.1
+description: "Substrate: The platform for blockchain innovators"
+home: https://substrate.network/
+icon: https://substrate.network/favicon.ico
+sources:
+  - https://github.com/paritytech/substrate/
+maintainers:
+  - name: Paritytech Devops Team
+    email: devops-team@parity.io
+tillerVersion: ">=2.8.0"
diff --git a/substrate/scripts/kubernetes/README.md b/substrate/scripts/kubernetes/README.md
new file mode 100644
index 00000000000..0f3ec389903
--- /dev/null
+++ b/substrate/scripts/kubernetes/README.md
@@ -0,0 +1,47 @@
+
+
+# Substrate Kubernetes Helm Chart
+
+This [Helm Chart](https://helm.sh/) can be used for deploying containerized 
+**Substrate** to a [Kubernetes](https://kubernetes.io/) cluster.
+
+
+## Prerequisites
+
+- Tested on Kubernetes 1.10.7-gke.6
+
+## Installation
+
+To install the chart with the release name `my-release` into namespace 
+`my-namespace` from within this directory:
+
+```console
+$ helm install --namespace my-namespace --name my-release --values values.yaml ./
+```
+
+The command deploys Substrate on the Kubernetes cluster in the configuration 
+given in `values.yaml`. When the namespace is omitted it'll be installed in 
+the default one.
+
+
+## Removal of the Chart
+
+To uninstall/delete the `my-release` deployment:
+
+```console
+$ helm delete --namespace my-namespace my-release
+```
+
+The command removes all the Kubernetes components associated with the chart and deletes the release.
+
+
+## Upgrading
+
+Once the chart is installed and a new version should be deployed helm takes 
+care of this by
+
+```console
+$ helm upgrade --namespace my-namespace --values values.yaml my-release ./
+```
+
+
diff --git a/substrate/scripts/kubernetes/templates/poddisruptionbudget.yaml b/substrate/scripts/kubernetes/templates/poddisruptionbudget.yaml
new file mode 100644
index 00000000000..af40522842c
--- /dev/null
+++ b/substrate/scripts/kubernetes/templates/poddisruptionbudget.yaml
@@ -0,0 +1,10 @@
+apiVersion: policy/v1beta1
+kind: PodDisruptionBudget
+metadata:
+  name: substrate
+spec:
+  selector:
+    matchLabels:
+      app: substrate
+  maxUnavailable: 1
+
diff --git a/substrate/scripts/kubernetes/templates/secrets.yaml b/substrate/scripts/kubernetes/templates/secrets.yaml
new file mode 100644
index 00000000000..e0073844825
--- /dev/null
+++ b/substrate/scripts/kubernetes/templates/secrets.yaml
@@ -0,0 +1,11 @@
+{{- if .Values.validator.keys }}
+apiVersion: v1
+kind: Secret
+metadata:
+  name: substrate-secrets
+  labels:
+    app: substrate
+type: Opaque
+data:
+  secrets: {{ .Values.validator.keys | default "" }}
+{{- end }}
diff --git a/substrate/scripts/kubernetes/templates/service.yaml b/substrate/scripts/kubernetes/templates/service.yaml
new file mode 100644
index 00000000000..27baa59dc4e
--- /dev/null
+++ b/substrate/scripts/kubernetes/templates/service.yaml
@@ -0,0 +1,39 @@
+# see:
+# https://kubernetes.io/docs/tutorials/services/
+# https://kubernetes.io/docs/concepts/services-networking/service/
+# headless service for rpc
+apiVersion: v1
+kind: Service
+metadata:
+  name: substrate-rpc
+  labels:
+    app: substrate
+spec:
+  ports:
+  - port: 9933
+    name: http-rpc
+  - port: 9944
+    name: websocket-rpc
+  selector:
+    app: substrate
+  sessionAffinity: None
+  type: ClusterIP
+  clusterIP: None
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: substrate
+spec:
+  ports:
+  - port: 30333
+    name: p2p
+    nodePort: 30333
+    protocol: TCP
+  selector:
+    app: substrate
+  sessionAffinity: None
+  type: NodePort
+  # don't route exteral traffic to non-local pods
+  externalTrafficPolicy: Local
+
diff --git a/substrate/scripts/kubernetes/templates/serviceaccount.yaml b/substrate/scripts/kubernetes/templates/serviceaccount.yaml
new file mode 100644
index 00000000000..5a0018a121b
--- /dev/null
+++ b/substrate/scripts/kubernetes/templates/serviceaccount.yaml
@@ -0,0 +1,10 @@
+{{- if .Values.rbac.enable }}
+# service account for substrate pods themselves
+# no permissions for the api are required
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  labels:
+    app: substrate
+  name: {{ .Values.rbac.name }}
+{{- end }}
diff --git a/substrate/scripts/kubernetes/templates/statefulset.yaml b/substrate/scripts/kubernetes/templates/statefulset.yaml
new file mode 100644
index 00000000000..ec491f63494
--- /dev/null
+++ b/substrate/scripts/kubernetes/templates/statefulset.yaml
@@ -0,0 +1,135 @@
+# https://kubernetes.io/docs/tutorials/stateful-application/basic-stateful-set/
+# https://cloud.google.com/kubernetes-engine/docs/concepts/statefulset
+apiVersion: apps/v1
+kind: StatefulSet
+metadata:
+  name: substrate
+spec:
+  selector:
+    matchLabels:
+      app: substrate
+  serviceName: substrate
+  replicas: {{ .Values.nodes.replicas }}
+  updateStrategy:
+    type: RollingUpdate
+  podManagementPolicy: Parallel
+  template:
+    metadata:
+      labels:
+        app: substrate
+    spec:
+      {{- if .Values.rbac.enable }}
+      serviceAccountName: {{ .Values.rbac.name }}
+      {{- else }}
+      serviceAccountName: default
+      {{- end }}
+      affinity:
+        nodeAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            nodeSelectorTerms:
+            - matchExpressions:
+                - key: node
+                  operator: In
+                  values:
+                  - substrate
+        podAntiAffinity:
+          requiredDuringSchedulingIgnoredDuringExecution:
+            - labelSelector:
+                matchExpressions:
+                  - key: "app"
+                    operator: In
+                    values:
+                    - substrate
+              topologyKey: "kubernetes.io/hostname"
+      terminationGracePeriodSeconds: 300
+      {{- if .Values.validator.keys }}
+      volumes:
+        - name: substrate-validator-secrets
+          secret:
+            secretName: substrate-secrets
+      initContainers:
+      - name: prepare-secrets
+        image: busybox
+        command: [ "/bin/sh" ]
+        args:
+          - -c
+          - sed -n -r "s/^${POD_NAME}-key ([^ ]+)$/\1/p" /etc/validator/secrets > {{ .Values.image.basepath }}/key;
+            sed -n -r "s/^${POD_NAME}-node-key ([^ ]+)$/\1/p" /etc/validator/secrets > {{ .Values.image.basepath }}/node-key
+        env:
+          # from (workaround for hostname)
+          # https://kubernetes.io/docs/tasks/inject-data-application/environment-variable-expose-pod-information/
+          - name: POD_NAME
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.name
+        volumeMounts:
+        - name: substrate-validator-secrets
+          readOnly: true
+          mountPath: "/etc/validator"
+        - name: substratedir
+          mountPath: {{ .Values.image.basepath }}
+      {{- end }}
+      containers:
+      - name: substrate
+        imagePullPolicy: "{{ .Values.image.pullPolicy }}"
+        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
+        {{- if .Values.resources }}
+        resources:
+          requests:
+            memory: {{ .Values.resources.memory }}
+            cpu: {{ .Values.resources.cpu }}
+        {{- end }}
+        ports:
+        - containerPort: 30333
+          name: p2p
+        - containerPort: 9933
+          name: http-rpc
+        - containerPort: 9944
+          name: websocket-rpc
+        command: ["/bin/sh"]
+        args:
+          - -c
+          - exec /usr/local/bin/substrate
+            --base-path {{ .Values.image.basepath }}
+            --name $(POD_NAME)
+            {{- if .Values.validator.enable }}
+            --validator
+            {{- end }}
+            {{- if .Values.validator.keys }}
+            --key $(cat {{ .Values.image.basepath }}/key)
+            --node-key $(cat {{ .Values.image.basepath }}/node-key)
+            {{- end }}
+            {{- range .Values.nodes.args }} {{ . }} {{- end }}
+        env:
+          - name: POD_NAME
+            valueFrom:
+              fieldRef:
+                fieldPath: metadata.name
+        volumeMounts:
+        - name: substratedir
+          mountPath: {{ .Values.image.basepath }}
+        readinessProbe:
+          httpGet:
+            path: /health
+            port: http-rpc
+          initialDelaySeconds: 10
+          periodSeconds: 10
+        livenessProbe:
+          httpGet:
+            path: /health
+            port: http-rpc
+          initialDelaySeconds: 10
+          periodSeconds: 10
+      securityContext:
+        runAsUser: 1000
+        fsGroup: 1000
+  volumeClaimTemplates:
+  - metadata:
+      name: substratedir
+    spec:
+      accessModes: [ "ReadWriteOnce" ]
+      storageClassName: ssd
+      resources:
+        requests:
+          storage: 32Gi
+
diff --git a/substrate/scripts/kubernetes/values.yaml b/substrate/scripts/kubernetes/values.yaml
new file mode 100644
index 00000000000..213ccaebe47
--- /dev/null
+++ b/substrate/scripts/kubernetes/values.yaml
@@ -0,0 +1,53 @@
+# set tag manually --set image.tag=latest
+image:
+  repository: parity/substrate
+  tag: latest
+  pullPolicy: Always
+  basepath: /substrate
+
+
+# if set to true a service account for substrate will be created
+rbac:
+  enable: true
+  name: substrate
+
+
+nodes:
+  replicas: 2
+  args:
+    # name and data directory are set by the chart itself
+    # key and node-key may be provided on commandline invocation
+    #
+    # - --chain
+    # - krummelanke
+    # serve rpc within the local network
+    # - fenced off the world via firewall
+    # - used for health checks
+    - --rpc-external
+    - --ws-external
+    # - --log
+    # - sub-libp2p=trace
+  
+
+validator:
+  enable: True
+  # adds --validator commandline option
+  #
+  # key and node-key can be given in a base64 encoded keyfile string (at 
+  # validator.keys) which has the following format:
+  #
+  # substrate-0-key <key-seed>
+  # substrate-0-node-key <node-secret-key>
+  # substrate-1-key <key-seed>
+  # substrate-1-node-key <node-secret-key>
+  #
+  # pod names are canonical. changing these or providing different amount of 
+  # keys than the replicas count will lead to behaviour noone ever has 
+  # experienced before.
+
+
+# maybe adopt resource limits here to the nodes of the pool
+# resources:
+#   memory: "5Gi"
+#   cpu: "1.5"
+
-- 
GitLab