Skip to content

massdriver-cloud/provisioner-helm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

helm

Massdriver Helm Provisioner

Massdriver provisioner for managing resources with Helm.

Structure

This provisioner expects the path to be the base directory of a helm chart. This means it should contain the Chart.yaml and values.yaml files at a minimium.

Tooling

The following tools are included in this provisioner:

  • Checkov: Included to scan helm charts for common policy and compliance violations.

Configuration

The following configuration options are available:

Configuration Option Type Default Description
kubernetes_cluster object .connections.kubernetes_cluster jq path to a massdriver/kubernetes-cluster connection for authentication to Kubernetes
namespace string "default" Kubernetes namespace to install the chart into. Defaults to the default namespace
release_name string <package name> Specifies the release name for the helm chart. Defaults to the Massdriver package name if not specified.
checkov.enable boolean true Enables Checkov policy evaluation. If false, Checkov will not be run.
checkov.quiet boolean true Only display failed checks if true (adds the --quiet flag).
checkov.halt_on_failure boolean false Halt provisioning run and mark deployment as failed on a policy failure (removes the --soft-fail flag).

Inputs

Helm accepts inputs via YAML formatted files, the primary one being values.yaml, though additional files can be specified. To adhere to this standard, this provisioner will convert the params.json and connections.json files into YAML format before passing them to Helm.

If modifications to params or connections are required to fit the predefined values of a helm chart, this provisioner supports JQ templates for restructuring both the params.json and connections.json files before they are converted to YAML. These JQ template files should exist in the base directory of the helm chart and be named params.jq and connections.jq, respectively. The format of these files should be a JQ template which accepts the params.json and connections.json files as inputs and restructures them according to the JQ template. These files aren't required by the provisioner so if either of them is missing the corresponding JSON file will be left unmodified.

To demonstrate, let's say there is a Helm bundle with some configuration values and a dependency on a Postgres database. The values.yaml file would be something like this:

commonLabels: {}

foo:
    bar: "baz"
    count: 4

postgres:
    hostname: ""
    port: 5432
    user: "root"
    password: ""
    version: "12.1"

To properly set these values in a Massdriver bundle, we likely would want the commonLabels value to come from md_metadata.default_tags, the foo value to come from params, and the postgres block to come from a connection. That means this bundle would require a massdriver/postgres-authentication connection named database. Since this is a Helm chart, it will also need a massdriver/kubernetes-cluster connection to provide authentication to the kubernetes cluster the chart is being installed into. The massdriver.yaml file would look something like:

params:
  required:
    - foo
  properties:
    foo:
      required:
        - bar
        - count
      properties:
        bar:
          type: string
        count:
          type: integer

connections:
  required:
    - kubernetes_cluster
    - database
  properties:
    kubernetes_cluster:
      $ref: massdriver/kubernetes-cluster
    database:
      $ref: massdriver/postgresql-authentication

params.jq

Let's start with the params.json, which will look like:

{
    "foo": {
        "bar": "bizzle",
        "count": 10
    },
    "md_metadata": {
        "default_tags": {
            "managed-by": "massdriver",
            "md-manifest": "somebundle",
            "md-package": "proj-env-somebundle-0000",
            "md-project": "proj",
            "md-target": "env"
        },
        "name_prefix": "proj-env-somebundle-0000"
        ...
    }
}

The foo object can be passed directly to helm chart since it already matches the structure in values.yaml. However, we want set commonLabels to md_metadata.default_tags, and we'd also like to remove the rest of md_metadata from the params since it isn't expected by the helm chart and could cause issues in the unlikely event there is a naming collision with an existing value named md_metadata. This means the params.jq file should contain:

. += {"commonLabels": .md_metadata.default_tags} | del(.md_metadata)

This JQ command takes all of the original JSON and adds the field commonLabels which is set to .md_metadata.default_tags. It then deletes the entire .md_metadata block from the params. The resulting params.yaml after this JQ restructuring and conversion to YAML would be:

commonLabels:
    managed-by: "massdriver",
    md-manifest: "somebundle",
    md-package: "proj-env-somebundle-0000",
    md-project: "proj",
    md-target: "env"
foo:
    bar: "bizzle"
    count: 10

This fits what the helm chart expects. Now let's focus on connections.

connections.jq

With the database and kubernetes_cluster connection, the connections.json file would be roughly equivalent to:

{
    "kubernetes_cluster": {
        "data": {
            "authentication": {
                "cluster": {
                    "certificate-authority-data": "...",
                    "server": "https://my.kubernetes.cluster.com"
                },
                "user": {
                    "token": "..."
                }
            }
        },
        "specs": {
            "kubernetes": {
                "version": "1.27"
            }
        }
    },
    "database": {
        "data": {
            "authentication": {
                "hostname": "the.postgres.database",
                "password": "s3cr3tV@lue",
                "port": 5432,
                "username": "admin"
            }
        },
        "specs": {
            "rdbms": {
                "version": "14.6"
            }
        }
    }
}

While this connections.json file contains all the necessary data for the postgres configuration, it isn't formatted properly and there is significantly more data than needed by the chart. The entire kubernetes_cluster block isn't used by the Helm chart at all (it is only needed to provide the provisioner with authentication information to the Kubernetes cluster). Let's create a connections.jq file to remove the kubernetes_cluster connection, and restructure the database connection so that it fits the helm chart's expected postgres block.

{
    "postgres": {
        "hostname": .database.data.authentication.hostname
        "port": .database.data.authentication.port
        "user": .database.data.authentication.username
        "password": .database.data.authentication.password
        "version": .database.specs.version
    }
}

This will restructure the data so that the connections.yaml file passed to helm will be:

postgres:
    hostname: "the.postgres.database"
    port: 5432
    user: "admin"
    password: "s3cr3tV@lue"
    version: "14.6"

This converts the data in connections.json to match the expected fields in values.yaml.

Artifacts

After every provision, this provider will scan the template directory for files matching the pattern artifact_<name>.jq. If a file matching this pattern is present, it will be used as a JQ template to render and publish a Massdriver artifact. The inputs to the JQ template will be a JSON object with the params, connections and helm manifests as top level fields. Note that the params and connections will contain the original content of params.json and connections.json, without any modifications that may have been applied through params.jq and connections.jq. The outputs field will contain the result of helm get manifest for the chart after it is installed. Since the output of helm get manifest is list of yaml files, the outputs block will be a JSON array with each element being a JSON object of an individual kubernetes resource manifest.

{
    "params": {
        ...
    },
    "connections": {
        ...
    },
    "outputs": [
        ...
    ]
}

To demonstrate, let's say there is a Helm bundle with a single param (namespace), a single connection (kubernetes_cluster), and a single artifact (api_endpoint). The massdriver.yaml would be similar to:

params:
  required:
    - namespace
  properties:
    namespace:
      type: string

connections:
  required:
    - kubernetes_cluster
  properties:
    kubernetes_cluster:
      $ref: massdriver/kubernetes-cluster

artifacts:
  required:
    - api_endpoint
  properties:
    api_endpoint:
      $ref: massdriver/api

Since the artifact is named api_endpoint a file named artifact_api_endpoint.jq would need to be in the template directory and the provisioner would use this file as a JQ template, passing the params, connections and outputs to it. For this example, let's say the helm chart will produce two manifests: a deployment, and a service. The output of helm get manifest would be something like:

---
apiVersion: v1
kind: Service
metadata:
  name: helm-prov-example-0000
spec:
  type: ClusterIP
  ports:
    - port: 80
      targetPort: 80
      protocol: TCP
      name: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: helm-prov-example-0000
spec:
  template:
    spec:
      containers:
        - name: nginx
          image: "nginx:latest"
          imagePullPolicy: Always

In this case, the input to the artifact_api_endpoint.jq template file would be:

{
    "params": {
        "namespace": "foo"
    },
    "connections": {
        "kubernetes_cluster": {
            "data": {
                "authentication": {
                    "cluster": {
                        "certificate-authority-data": "...",
                        "server": "https://my.kubernetes.cluster.com"
                    },
                    "user": {
                        "token": "..."
                    }
                }
            },
            "specs": {
                "kubernetes": {
                    "version": "1.27"
                }
            }
        }
    },
    "outputs": [
        {
            "apiVersion": "v1",
            "kind": "Service",
            "metadata": {
                "name": "helm-prov-example-0000"
            },
            "spec": {
                "type": "ClusterIP",
                "ports": [{
                    "port": 80,
                    "targetPort": 80,
                    "protocol": "TCP",
                    "name": "http"
                }]
            }
        },
        {
            "apiVersion": "apps/v1",
            "kind": "Deployment",
            "metadata": {
                "name": "helm-prov-example-0000"
            },
            "spec": {
                "template": {
                "spec": {
                    "containers": [
                    {
                        "name": "nginx",
                        "image": "nginx:latest",
                        "imagePullPolicy": "Always"
                    }
                    ]
                }
                }
            }
        }
    ]
}

We need to build an API artifact from these inputs. We'll use Kubernetes built in DNS pattern for services to build the API endpoint from the service name, namespace and port. Thus, the artifact_api_endpoint.jq file would be:

{
    "data": {
        "api": {
            "hostname": "\(.outputs[] | select(.kind == "Service" and .apiVersion == "v1") | .metadata.name).\(.params.namespace).svc.cluster.local",
            "port": (.outputs[] | select(.kind == "Service" and .apiVersion == "v1") | .spec.ports[] | select(.name == "http") | .port),
            "protocol": "http"
        }
    },
    "specs": {
        "api": {
            "version": "1.0.0"
        }
    }
}

In this template, we are using the select function in JQ to find the proper manifest and extract the relevant values to build a properly formatted artifact.

Releases

No releases published

Packages

No packages published