Repository: kinvolk/lokomotive
Pull request #981 (2020-09-17 12:48:05)

pkg/components: add Headlamp component 
Fixes https://github.com/kinvolk/lokomotive/issues/988

surajssd: @iaguis can you address the reviews. Or do you mind someone taking it over?
invidian: @iaguis make sure to pull the reviewers again when you are done addressing review comments. Resolving existing conversations when they are addressed is also helpful while doing another rounds of reviews :) 
iaguis: > @iaguis make sure to pull the reviewers again when you are done addressing review comments. Resolving existing conversations when they are addressed is also helpful while doing another rounds of reviews :)

Yes, I was waiting for CI to pass and I didn't realize it was passing already :D

Patch:
diff --git a/assets/charts/components/headlamp/.helmignore b/assets/charts/components/headlamp/.helmignore
new file mode 100644
index 00000000..0e8a0eb3
--- /dev/null
+++ b/assets/charts/components/headlamp/.helmignore
@@ -0,0 +1,23 @@
+# Patterns to ignore when building packages.
+# This supports shell glob matching, relative path matching, and
+# negation (prefixed with !). Only one pattern per line.
+.DS_Store
+# Common VCS dirs
+.git/
+.gitignore
+.bzr/
+.bzrignore
+.hg/
+.hgignore
+.svn/
+# Common backup files
+*.swp
+*.bak
+*.tmp
+*.orig
+*~
+# Various IDEs
+.project
+.idea/
+*.tmproj
+.vscode/
diff --git a/assets/charts/components/headlamp/Chart.yaml b/assets/charts/components/headlamp/Chart.yaml
new file mode 100644
index 00000000..503aaf3b
--- /dev/null
+++ b/assets/charts/components/headlamp/Chart.yaml
@@ -0,0 +1,7 @@
+apiVersion: v2
+name: headlamp
+description: An easy-to-use and versatile dashboard for Kubernetes brought to you by Kinvolk.
+type: application
+
+version: 0.1.0
+appVersion: 0.0.0
diff --git a/assets/charts/components/headlamp/templates/_helpers.tpl b/assets/charts/components/headlamp/templates/_helpers.tpl
new file mode 100644
index 00000000..47e103c5
--- /dev/null
+++ b/assets/charts/components/headlamp/templates/_helpers.tpl
@@ -0,0 +1,62 @@
+{{/*
+Expand the name of the chart.
+*/}}
+{{- define "headlamp.name" -}}
+{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Create a default fully qualified app name.
+We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec).
+If release name contains chart name it will be used as a full name.
+*/}}
+{{- define "headlamp.fullname" -}}
+{{- if .Values.fullnameOverride }}
+{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- $name := default .Chart.Name .Values.nameOverride }}
+{{- if contains $name .Release.Name }}
+{{- .Release.Name | trunc 63 | trimSuffix "-" }}
+{{- else }}
+{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }}
+{{- end }}
+{{- end }}
+{{- end }}
+
+{{/*
+Create chart name and version as used by the chart label.
+*/}}
+{{- define "headlamp.chart" -}}
+{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
+{{- end }}
+
+{{/*
+Common labels
+*/}}
+{{- define "headlamp.labels" -}}
+helm.sh/chart: {{ include "headlamp.chart" . }}
+{{ include "headlamp.selectorLabels" . }}
+{{- if .Chart.AppVersion }}
+app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
+{{- end }}
+app.kubernetes.io/managed-by: {{ .Release.Service }}
+{{- end }}
+
+{{/*
+Selector labels
+*/}}
+{{- define "headlamp.selectorLabels" -}}
+app.kubernetes.io/name: {{ include "headlamp.name" . }}
+app.kubernetes.io/instance: {{ .Release.Name }}
+{{- end }}
+
+{{/*
+Create the name of the service account to use
+*/}}
+{{- define "headlamp.serviceAccountName" -}}
+{{- if .Values.serviceAccount.create }}
+{{- default (include "headlamp.fullname" .) .Values.serviceAccount.name }}
+{{- else }}
+{{- default "default" .Values.serviceAccount.name }}
+{{- end }}
+{{- end }}
diff --git a/assets/charts/components/headlamp/templates/deployment.yaml b/assets/charts/components/headlamp/templates/deployment.yaml
new file mode 100644
index 00000000..5c91e6ba
--- /dev/null
+++ b/assets/charts/components/headlamp/templates/deployment.yaml
@@ -0,0 +1,60 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: {{ include "headlamp.fullname" . }}
+  labels:
+    {{- include "headlamp.labels" . | nindent 4 }}
+spec:
+  selector:
+    matchLabels:
+      {{- include "headlamp.selectorLabels" . | nindent 6 }}
+  template:
+    metadata:
+    {{- with .Values.podAnnotations }}
+      annotations:
+        {{- toYaml . | nindent 8 }}
+    {{- end }}
+      labels:
+        {{- include "headlamp.selectorLabels" . | nindent 8 }}
+    spec:
+      {{- with .Values.imagePullSecrets }}
+      imagePullSecrets:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      securityContext:
+        {{- toYaml .Values.podSecurityContext | nindent 8 }}
+      containers:
+        - name: {{ .Chart.Name }}
+          securityContext:
+            {{- toYaml .Values.securityContext | nindent 12 }}
+          image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}"
+          imagePullPolicy: {{ .Values.image.pullPolicy }}
+          args:
+          - -in-cluster
+          - -plugins-dir=/headlamp/plugins/
+          ports:
+            - name: http
+              containerPort: {{ .Values.service.targetPort }}
+              protocol: TCP
+          livenessProbe:
+            httpGet:
+              path: /
+              port: {{ .Values.service.targetPort }}
+          readinessProbe:
+            httpGet:
+              path: /
+              port: {{ .Values.service.targetPort }}
+          resources:
+            {{- toYaml .Values.resources | nindent 12 }}
+      {{- with .Values.nodeSelector }}
+      nodeSelector:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.affinity }}
+      affinity:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
+      {{- with .Values.tolerations }}
+      tolerations:
+        {{- toYaml . | nindent 8 }}
+      {{- end }}
diff --git a/assets/charts/components/headlamp/templates/ingress.yaml b/assets/charts/components/headlamp/templates/ingress.yaml
new file mode 100644
index 00000000..bd078544
--- /dev/null
+++ b/assets/charts/components/headlamp/templates/ingress.yaml
@@ -0,0 +1,41 @@
+{{- if .Values.ingress.enabled -}}
+{{- $fullName := include "headlamp.fullname" . -}}
+{{- $svcPort := .Values.service.port -}}
+{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}}
+apiVersion: networking.k8s.io/v1beta1
+{{- else -}}
+apiVersion: extensions/v1beta1
+{{- end }}
+kind: Ingress
+metadata:
+  name: {{ $fullName }}
+  labels:
+    {{- include "headlamp.labels" . | nindent 4 }}
+  {{- with .Values.ingress.annotations }}
+  annotations:
+    {{- toYaml . | nindent 4 }}
+  {{- end }}
+spec:
+  {{- if .Values.ingress.tls }}
+  tls:
+    {{- range .Values.ingress.tls }}
+    - hosts:
+        {{- range .hosts }}
+        - {{ . | quote }}
+        {{- end }}
+      secretName: {{ .secretName }}
+    {{- end }}
+  {{- end }}
+  rules:
+    {{- range .Values.ingress.hosts }}
+    - host: {{ .host | quote }}
+      http:
+        paths:
+          {{- range .paths }}
+          - path: {{ . }}
+            backend:
+              serviceName: {{ $fullName }}
+              servicePort: {{ $svcPort }}
+          {{- end }}
+    {{- end }}
+  {{- end }}
diff --git a/assets/charts/components/headlamp/templates/service.yaml b/assets/charts/components/headlamp/templates/service.yaml
new file mode 100644
index 00000000..00c7ff18
--- /dev/null
+++ b/assets/charts/components/headlamp/templates/service.yaml
@@ -0,0 +1,15 @@
+apiVersion: v1
+kind: Service
+metadata:
+  name: {{ include "headlamp.fullname" . }}
+  labels:
+    {{- include "headlamp.labels" . | nindent 4 }}
+spec:
+  type: {{ .Values.service.type }}
+  ports:
+    - port: {{ .Values.service.port }}
+      targetPort: {{ .Values.service.targetPort }}
+      protocol: TCP
+      name: http
+  selector:
+    {{- include "headlamp.selectorLabels" . | nindent 4 }}
diff --git a/assets/charts/components/headlamp/values.yaml b/assets/charts/components/headlamp/values.yaml
new file mode 100644
index 00000000..15495e85
--- /dev/null
+++ b/assets/charts/components/headlamp/values.yaml
@@ -0,0 +1,64 @@
+# Default values for headlamp.
+# This is a YAML-formatted file.
+# Declare variables to be passed into your templates.
+
+replicaCount: 1
+
+image:
+  repository: quay.io/kinvolk/headlamp
+  pullPolicy: IfNotPresent
+  # Overrides the image tag whose default is the chart appVersion.
+  tag: l8e
+
+imagePullSecrets: []
+nameOverride: ""
+fullnameOverride: ""
+
+podAnnotations: {}
+
+podSecurityContext: {}
+  # fsGroup: 2000
+
+securityContext: {}
+  # capabilities:
+  #   drop:
+  #   - ALL
+  # readOnlyRootFilesystem: true
+  # runAsNonRoot: true
+  # runAsUser: 1000
+
+service:
+  type: ClusterIP
+  port: 80
+  targetPort: 4466
+
+ingress:
+  enabled: false
+  annotations: {}
+    # kubernetes.io/ingress.class: nginx
+    # kubernetes.io/tls-acme: "true"
+  hosts:
+    - host: chart-example.local
+      paths: []
+  tls: []
+  #  - secretName: chart-example-tls
+  #    hosts:
+  #      - chart-example.local
+
+resources: {}
+  # We usually recommend not to specify default resources and to leave this as a conscious
+  # choice for the user. This also increases chances charts run on environments with little
+  # resources, such as Minikube. If you do want to specify resources, uncomment the following
+  # lines, adjust them as necessary, and remove the curly braces after 'resources:'.
+  # limits:
+  #   cpu: 100m
+  #   memory: 128Mi
+  # requests:
+  #   cpu: 100m
+  #   memory: 128Mi
+
+nodeSelector: {}
+
+tolerations: []
+
+affinity: {}
diff --git a/assets/terraform-modules/aws/flatcar-linux/kubernetes/versions.tf b/assets/terraform-modules/aws/flatcar-linux/kubernetes/versions.tf
index b1d21ece..2c0d64d6 100644
--- a/assets/terraform-modules/aws/flatcar-linux/kubernetes/versions.tf
+++ b/assets/terraform-modules/aws/flatcar-linux/kubernetes/versions.tf
@@ -1,28 +1,12 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    aws = {
-      source  = "hashicorp/aws"
-      version = "3.3.0"
-    }
-    ct = {
-      source  = "poseidon/ct"
-      version = "0.6.1"
-    }
-    null = {
-      source  = "hashicorp/null"
-      version = "2.1.2"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "2.3.0"
-    }
+    aws      = "3.3.0"
+    ct       = "0.6.1"
+    null     = "2.1.2"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/aws/flatcar-linux/kubernetes/workers/versions.tf b/assets/terraform-modules/aws/flatcar-linux/kubernetes/workers/versions.tf
index 29e3f62a..675cbc2b 100644
--- a/assets/terraform-modules/aws/flatcar-linux/kubernetes/workers/versions.tf
+++ b/assets/terraform-modules/aws/flatcar-linux/kubernetes/workers/versions.tf
@@ -1,24 +1,11 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    aws = {
-      source  = "hashicorp/aws"
-      version = "3.3.0"
-    }
-    ct = {
-      source  = "poseidon/ct"
-      version = "0.6.1"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "2.3.0"
-    }
+    aws      = "3.3.0"
+    ct       = "0.6.1"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/azure/flatcar-linux/kubernetes/versions.tf b/assets/terraform-modules/azure/flatcar-linux/kubernetes/versions.tf
index ecb82433..9e1553d3 100644
--- a/assets/terraform-modules/azure/flatcar-linux/kubernetes/versions.tf
+++ b/assets/terraform-modules/azure/flatcar-linux/kubernetes/versions.tf
@@ -1,24 +1,12 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    ct = {
-      source = "poseidon/ct"
-      version = "0.6.0"
-    }
-    azurerm = {
-      source  = "hashicorp/azurerm"
-      version = "1.35.0"
-    }
-    null = {
-      source  = "hashicorp/null"
-      version = "2.1.2"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
+    ct       = "0.6.0"
+    azurerm  = "1.35.0"
+    null     = "2.1.2"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/azure/flatcar-linux/kubernetes/workers/versions.tf b/assets/terraform-modules/azure/flatcar-linux/kubernetes/workers/versions.tf
index c5c56c7f..711d2ee5 100644
--- a/assets/terraform-modules/azure/flatcar-linux/kubernetes/workers/versions.tf
+++ b/assets/terraform-modules/azure/flatcar-linux/kubernetes/workers/versions.tf
@@ -1,18 +1,9 @@
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    ct = {
-      source = "poseidon/ct"
-      version = "0.6.0"
-    }
-    azurerm = {
-      source  = "hashicorp/azurerm"
-      version = "1.35.0"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
+    ct       = "0.6.0"
+    azurerm  = "1.35.0"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/bare-metal/flatcar-linux/kubernetes/versions.tf b/assets/terraform-modules/bare-metal/flatcar-linux/kubernetes/versions.tf
index 3f04f473..cab2696d 100644
--- a/assets/terraform-modules/bare-metal/flatcar-linux/kubernetes/versions.tf
+++ b/assets/terraform-modules/bare-metal/flatcar-linux/kubernetes/versions.tf
@@ -1,28 +1,12 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    ct = {
-      source  = "poseidon/ct"
-      version = "0.6.1"
-    }
-    null = {
-      source  = "hashicorp/null"
-      version = "2.1.2"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    matchbox = {
-      source  = "poseidon/matchbox"
-      version = "0.4.1"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "2.3.0"
-    }
+    ct       = "0.6.1"
+    null     = "2.1.2"
+    template = "2.1.2"
+    matchbox = "0.4.1"
   }
 }
diff --git a/assets/terraform-modules/bootkube/versions.tf b/assets/terraform-modules/bootkube/versions.tf
index 97891821..f9672fa7 100644
--- a/assets/terraform-modules/bootkube/versions.tf
+++ b/assets/terraform-modules/bootkube/versions.tf
@@ -1,19 +1,11 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12"
   required_providers {
-    local = {
-      source  = "hashicorp/local"
-      version = "1.4.0"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    tls = {
-      source  = "hashicorp/tls"
-      version = "2.2.0"
-    }
+    local    = "1.4.0"
+    template = "2.1.2"
+    tls      = "2.2.0"
+    random   = "2.3.0"
   }
 }
diff --git a/assets/terraform-modules/dns/cloudflare/versions.tf b/assets/terraform-modules/dns/cloudflare/versions.tf
index 71a3832b..ef82d1e5 100644
--- a/assets/terraform-modules/dns/cloudflare/versions.tf
+++ b/assets/terraform-modules/dns/cloudflare/versions.tf
@@ -1,10 +1,7 @@
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    cloudflare = {
-      source  = "cloudflare/cloudflare"
-      version = "2.9.0"
-    }
+    cloudflare = "2.9.0"
   }
 }
diff --git a/assets/terraform-modules/dns/route53/versions.tf b/assets/terraform-modules/dns/route53/versions.tf
index c404d482..92ab0f9f 100644
--- a/assets/terraform-modules/dns/route53/versions.tf
+++ b/assets/terraform-modules/dns/route53/versions.tf
@@ -1,10 +1,7 @@
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    aws = {
-      source  = "hashicorp/aws"
-      version = "3.3.0"
-    }
+    aws = "3.3.0"
   }
 }
diff --git a/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/versions.tf b/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/versions.tf
index 3d44753b..5a1dfabd 100644
--- a/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/versions.tf
+++ b/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/versions.tf
@@ -1,23 +1,11 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    google = {
-      source  = "hashicorp/google"
-      version = "2.16.0"
-    }
-    null = {
-      source  = "hashicorp/null"
-      version = "2.1.2"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    ct = {
-      source = "poseidon/ct"
-    }
+    google   = "2.16.0"
+    null     = "2.1.2"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/versions.tf b/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/versions.tf
index 12461fc0..6dd232b3 100644
--- a/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/versions.tf
+++ b/assets/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/versions.tf
@@ -1,17 +1,8 @@
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    google = {
-      source  = "hashicorp/google"
-      version = "2.16.0"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    ct = {
-      source = "poseidon/ct"
-    }
+    google   = "2.16.0"
+    template = "2.1.2"
   }
 }
diff --git a/assets/terraform-modules/packet/flatcar-linux/kubernetes/versions.tf b/assets/terraform-modules/packet/flatcar-linux/kubernetes/versions.tf
index 05c67847..d3fb2c39 100644
--- a/assets/terraform-modules/packet/flatcar-linux/kubernetes/versions.tf
+++ b/assets/terraform-modules/packet/flatcar-linux/kubernetes/versions.tf
@@ -1,32 +1,13 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    ct = {
-      source  = "poseidon/ct"
-      version = "0.6.1"
-    }
-    local = {
-      source  = "hashicorp/local"
-      version = "1.4.0"
-    }
-    null = {
-      source  = "hashicorp/null"
-      version = "2.1.2"
-    }
-    template = {
-      source  = "hashicorp/template"
-      version = "2.1.2"
-    }
-    packet = {
-      source  = "packethost/packet"
-      version = "3.0.1"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "2.3.0"
-    }
+    ct       = "0.6.1"
+    local    = "1.4.0"
+    null     = "2.1.2"
+    template = "2.1.2"
+    packet   = "3.0.0"
   }
 }
diff --git a/assets/terraform-modules/packet/flatcar-linux/kubernetes/workers/versions.tf b/assets/terraform-modules/packet/flatcar-linux/kubernetes/workers/versions.tf
index cade00c3..26d29a9d 100644
--- a/assets/terraform-modules/packet/flatcar-linux/kubernetes/workers/versions.tf
+++ b/assets/terraform-modules/packet/flatcar-linux/kubernetes/workers/versions.tf
@@ -1,20 +1,10 @@
 # Terraform version and plugin versions
 
 terraform {
-  required_version = ">= 0.13"
+  required_version = ">= 0.12.0"
 
   required_providers {
-    ct = {
-      source  = "poseidon/ct"
-      version = "0.6.1"
-    }
-    packet = {
-      source  = "packethost/packet"
-      version = "3.0.1"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "2.3.0"
-    }
+    ct     = "0.6.1"
+    packet = "3.0.0"
   }
 }
diff --git a/ci/aks/aks-cluster.lokocfg.envsubst b/ci/aks/aks-cluster.lokocfg.envsubst
index 20431840..3612025b 100644
--- a/ci/aks/aks-cluster.lokocfg.envsubst
+++ b/ci/aks/aks-cluster.lokocfg.envsubst
@@ -118,3 +118,10 @@ component "httpbin" {
 component "experimental-istio-operator" {
   enable_monitoring = true
 }
+
+component "headlamp" {
+  ingress {
+    host                       = "headlamp.${var.cluster_name}.${var.aws_dns_zone}"
+    certmanager_cluster_issuer = "letsencrypt-staging"
+  }
+}
diff --git a/ci/aws/aws-cluster.lokocfg.envsubst b/ci/aws/aws-cluster.lokocfg.envsubst
index ebd15b59..a94df0ca 100644
--- a/ci/aws/aws-cluster.lokocfg.envsubst
+++ b/ci/aws/aws-cluster.lokocfg.envsubst
@@ -236,3 +236,10 @@ component "aws-ebs-csi-driver" {}
 component "experimental-istio-operator" {
   enable_monitoring = true
 }
+
+component "headlamp" {
+  ingress {
+    host                       = "headlamp.$CLUSTER_ID.$AWS_DNS_ZONE"
+    certmanager_cluster_issuer = "letsencrypt-staging"
+  }
+}
diff --git a/ci/packet/packet-cluster.lokocfg.envsubst b/ci/packet/packet-cluster.lokocfg.envsubst
index 966e71e9..720208a5 100644
--- a/ci/packet/packet-cluster.lokocfg.envsubst
+++ b/ci/packet/packet-cluster.lokocfg.envsubst
@@ -208,3 +208,10 @@ component "httpbin" {
 component "experimental-istio-operator" {
   enable_monitoring = true
 }
+
+component "headlamp" {
+  ingress {
+    host                       = "headlamp.$CLUSTER_ID.$AWS_DNS_ZONE"
+    certmanager_cluster_issuer = "letsencrypt-staging"
+  }
+}
diff --git a/ci/set-terraform-version b/ci/set-terraform-version
deleted file mode 100755
index da6aefba..00000000
--- a/ci/set-terraform-version
+++ /dev/null
@@ -1,2 +0,0 @@
-#!/bin/sh
-ln -fs ~/.local/bin/terraform13 ~/.local/bin/terraform
diff --git a/cli/cmd/component.go b/cli/cmd/component.go
index 9eeec84f..fea27143 100644
--- a/cli/cmd/component.go
+++ b/cli/cmd/component.go
@@ -26,6 +26,7 @@ import (
 	_ "github.com/kinvolk/lokomotive/pkg/components/external-dns"
 	_ "github.com/kinvolk/lokomotive/pkg/components/flatcar-linux-update-operator"
 	_ "github.com/kinvolk/lokomotive/pkg/components/gangway"
+	_ "github.com/kinvolk/lokomotive/pkg/components/headlamp"
 	_ "github.com/kinvolk/lokomotive/pkg/components/httpbin"
 	_ "github.com/kinvolk/lokomotive/pkg/components/istio-operator"
 	_ "github.com/kinvolk/lokomotive/pkg/components/linkerd"
diff --git a/docs/KEYS.md b/docs/KEYS.md
index 150b6d5c..83d15cf8 100644
--- a/docs/KEYS.md
+++ b/docs/KEYS.md
@@ -11,11 +11,9 @@ This is the current set of trusted keys:
 | [0xF79374C3][key-invidian]      | `9725 A27E 0A93 A153 B798 F292 8DBC FB00 F793 74C3`   | @invidian     |
 | [0xF82B446B][key-ipochi]        | `967F A608 7E62 DF09 E22A 3A40 2851 51CE F82B 446B`   | @ipochi       |
 | [0xDF48CA1E][key-surajssd]      | `17E6 829F 7335 E994 2613 AABE 83E9 8295 DF48 CA1E`   | @surajssd     |
-| [0x9B8CA58D][key-knrt10]        | `5ACC 9A58 DEED 7E03 9F32 4802 B750 D68F 9B8C A58D`   | @knrt10       |
 
 
 [key-iaguis]: https://pgp.mit.edu/pks/lookup?search=0xB7DDBE7001408D7D7FD079D27E26368261BAD4F7&op=index&exact=on
 [key-invidian]: https://pgp.mit.edu/pks/lookup?search=0x9725A27E0A93A153B798F2928DBCFB00F79374C3&op=index&exact=on
 [key-ipochi]: https://pgp.mit.edu/pks/lookup?search=0x967FA6087E62DF09E22A3A40285151CEF82B446B&op=index&exact=on
 [key-surajssd]: https://pgp.mit.edu/pks/lookup?search=0x17E6829F7335E9942613AABE83E98295DF48CA1E&op=index&exact=on
-[key-knrt10]: https://pgp.mit.edu/pks/lookup?search=0x5ACC9A58DEED7E039F324802B750D68F9B8CA58D&op=index&exact=on
diff --git a/docs/concepts/components.md b/docs/concepts/components.md
index 59157024..bd231b22 100644
--- a/docs/concepts/components.md
+++ b/docs/concepts/components.md
@@ -74,13 +74,17 @@ Sample output:
 
 ```
 Available components:
+	 aws-ebs-csi-driver
 	 cert-manager
 	 cluster-autoscaler
 	 contour
 	 dex
+	 experimental-istio-operator
+	 experimental-linkerd
 	 external-dns
 	 flatcar-linux-update-operator
 	 gangway
+	 headlamp
 	 httpbin
 	 metallb
 	 metrics-server
diff --git a/docs/configuration-reference/components/headlamp.md b/docs/configuration-reference/components/headlamp.md
new file mode 100644
index 00000000..8982fe42
--- /dev/null
+++ b/docs/configuration-reference/components/headlamp.md
@@ -0,0 +1,70 @@
+# Headlamp configuration reference for Lokomotive
+
+## Contents
+
+* [Introduction](#introduction)
+* [Prerequisites](#prerequisites)
+* [Configuration](#configuration)
+* [Attribute reference](#attribute-reference)
+* [Applying](#applying)
+* [Deleting](#deleting)
+
+## Introduction
+
+[Headlamp](https://github.com/kinvolk/headlamp) is an easy-to-use and versatile
+dashboard for Kubernetes.
+
+It has a clean and modern UI, it is vendor independent, generic, and supports
+the most common operations for Kubernetes clusters.
+
+## Prerequisites
+
+* A Kubernetes cluster accessible via `kubectl`.
+
+* An ingress controller such as [Contour](contour.md) for HTTP ingress.
+
+* [cert-manager](cert-manager.md) to generate TLS certificates.
+
+## Configuration
+
+```tf
+# headlamp.lokocfg
+
+component "headlamp" {
+  ingress {
+    host                       = "headlamp.example.lokomotive-k8s.org"
+    class                      = "contour"
+    certmanager_cluster_issuer = "letsencrypt-production"
+  }
+}
+```
+
+## Attribute reference
+
+Table of all the arguments accepted by the component.
+
+Example:
+
+| Argument                             | Description                                                                                                                                   | Default                  | Type   | Required |
+|--------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|--:-:---------------------|--:-:---|--:-:-----|
+| `namespace`                          | Namespace where Headlamp will be installed.                                                                                                   | "lokomotive-system"      | string | false    |
+| `ingress`                            | Configuration block for exposing Headlamp through an Ingress resource.                                                                        | -                        | block  | false    |
+| `ingress.host`                       | Used as the `hosts` domain in the Ingress resource for headlamp that is automatically created.                                                | -                        | string | true     |
+| `ingress.class`                      | Ingress class to use for the Headlamp Ingress.                                                                                                | `contour`                | string | false    |
+| `ingress.certmanager_cluster_issuer` | `ClusterIssuer` to be used by cert-manager while issuing TLS certificates. Supported values: `letsencrypt-production`, `letsencrypt-staging`. | `letsencrypt-production` | string | false    |
+
+## Applying
+
+To apply the Headlamp component:
+
+```bash
+lokoctl component apply headlamp
+```
+
+## Deleting
+
+To destroy the component:
+
+```bash
+lokoctl component delete headlamp
+```
diff --git a/docs/configuration-reference/components/metallb.md b/docs/configuration-reference/components/metallb.md
index 2f966a92..3116e7f1 100644
--- a/docs/configuration-reference/components/metallb.md
+++ b/docs/configuration-reference/components/metallb.md
@@ -50,8 +50,8 @@ MetalLB component configuration example:
 ```tf
 component "metallb" {
   address_pools = {
-    pool1 = ["147.63.8.20/32"]
-    pool2 = ["147.85.47.16/29", "147.85.47.24/29"]
+    default = ["147.63.8.20/32"]
+    special_addresses = ["147.85.47.16/29", "147.85.47.24/29"]
   }
   controller_node_selectors = {
     "kubernetes.io/hostname" = "worker3"
@@ -77,28 +77,6 @@ component "metallb" {
 
 MetalLB will use the specified CIDR for exposing services of type `LoadBalancer`.
 
-### Advanced IP allocation
-
-By default, MetalLB uses all specified address pools to allocate IP addresses to services. To
-request an address from a specific pool, set the `metallb.universe.tf/address-pool` annotation for
-the relevant service:
-
-```yaml
-apiVersion: v1
-kind: Service
-metadata:
-  name: nginx
-  annotations:
-    metallb.universe.tf/address-pool: pool2
-spec:
-  ports:
-  - port: 80
-    targetPort: 80
-  selector:
-    app: nginx
-  type: LoadBalancer
-```
-
 ## Attribute reference
 
 Table of all the arguments accepted by the component.
@@ -107,7 +85,7 @@ Example:
 
 | Argument                    | Description                                                                                | Default | Type                                                                                                           | Required |
 |-----------------------------|--------------------------------------------------------------------------------------------|:-------:|:---------------------------------------------------------------------------------------------------------------|:--------:|
-| `address_pools`             | A map which allows specifying one or more CIDRs which MetalLB can use to expose services.  |    -    | map(list(string))                                                                                              |   true   |
+| `address_pools`             | A map which allows specifying one or more CIDRs which MetalLB can use to expose services.  |    -    | object({default = list(string), special_addresses = list(string)})                                             |   true   |
 | `controller_node_selectors` | A map with specific labels to run MetalLB controller pods selectively on a group of nodes. |    -    | map(string)                                                                                                    |  false   |
 | `speaker_node_selectors`    | A map with specific labels to run MetalLB speaker pods selectively on a group of nodes.    |    -    | map(string)                                                                                                    |  false   |
 | `controller_toleration`     | Specify one or more tolerations for controller pods.                                       |    -    | list(object({key = string, effect = string, operator = string, value = string, toleration_seconds = string })) |  false   |
@@ -122,7 +100,6 @@ To apply the MetalLB component:
 ```bash
 lokoctl component apply metallb
 ```
-
 ## Deleting
 
 To destroy the component:
diff --git a/docs/development/README.md b/docs/development/README.md
index f10f37a3..1cd8411e 100644
--- a/docs/development/README.md
+++ b/docs/development/README.md
@@ -4,8 +4,7 @@ This is a developer docs directory. Not meant for end users.
 
 ## Contents
 
-- [How to update components?](./updating-components.md)
-- [Lokomotive coding style guide](./coding-style-guide.md)
+- [Update component configuration.](./updating-components.md)
 
 ## Spell checking
 
diff --git a/go.mod b/go.mod
index 80ee601c..547bf885 100644
--- a/go.mod
+++ b/go.mod
@@ -21,7 +21,7 @@ require (
 	github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b
 	github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
 	github.com/golang/protobuf v1.3.5 // indirect
-	github.com/google/go-cmp v0.5.1 // indirect
+	github.com/google/go-cmp v0.5.1
 	github.com/googleapis/gnostic v0.4.1 // indirect
 	github.com/gorilla/mux v1.7.4 // indirect
 	github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect
diff --git a/pkg/assets/assets.go b/pkg/assets/assets.go
index 1a2b980e..11ce8acc 100644
--- a/pkg/assets/assets.go
+++ b/pkg/assets/assets.go
@@ -19,27 +19,13 @@ package assets
 import (
 	"fmt"
 	"io"
+	"net/http"
 	"os"
+	"time"
 
-	// Currently pkg/assets/assets_generate.go file is ignored from regular
-	// builds and it's only used when running 'go generate'.
-	//
-	// Because of that, external dependencies of assets_generate.go are not
-	// normally added to go.mod and go.sum files, unless you run 'go generate'.
-	// But then 'go mod tidy' removes them again.
-	//
-	// Those anonymous imports ensure, that those external dependencies
-	// will always be pulled into go.mod file.
-	//
-	// The alternative would be to refactor the function to actually use the
-	// dependencies here, but then we have an exported function, which
-	// shouldn't really be exported.
-	//
-	// Also see https://github.com/golang/go/issues/25922 and https://github.com/golang/go/issues/29516
-	// for more details about this issue.
-	_ "github.com/prometheus/alertmanager/pkg/modtimevfs"
-	_ "github.com/shurcooL/httpfs/union"
-	_ "github.com/shurcooL/vfsgen"
+	"github.com/prometheus/alertmanager/pkg/modtimevfs"
+	"github.com/shurcooL/httpfs/union"
+	"github.com/shurcooL/vfsgen"
 )
 
 const (
@@ -51,24 +37,46 @@ const (
 	TerraformModulesSource = "/terraform-modules"
 )
 
-type walkFunc func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error
+type WalkFunc func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error
 
-type assetsIface interface {
+type AssetsIface interface {
 	// WalkFiles calls cb for every regular file within path.
 	//
 	// Usually, fileName passed to the cb will be relative to
 	// path. But in case of error, it is possible that it will
 	// not.be relative. Also, in case of error, fileInfo or r may
 	// be nil.
-	WalkFiles(path string, cb walkFunc) error
+	WalkFiles(path string, cb WalkFunc) error
 }
 
-func get() assetsIface {
+var Assets AssetsIface
+
+func init() {
+	Assets = newEmbeddedAssets()
 	if value, found := os.LookupEnv("LOKOCTL_USE_FS_ASSETS"); found {
-		return newFsAssets(value)
+		Assets = newFsAssets(value)
 	}
+}
 
-	return newEmbeddedAssets()
+// Generate function wraps vfsgen.Generate function.
+// Additionally to vfsgen.Generate, it also takes map of directories,
+// where key represents path in the assets and a value represents path
+// to the assets directory in local filesystem (which should be relative).
+//
+// This function also resets modification time for every file, so creating a new copy
+// of code does not trigger changes in all asset files.
+func Generate(fileName string, packageName string, variableName string, dirs map[string]string) error {
+	ufs := make(map[string]http.FileSystem)
+	for assetsPath, fsPath := range dirs {
+		ufs[assetsPath] = http.Dir(fsPath)
+	}
+	u := union.New(ufs)
+	fs := modtimevfs.New(u, time.Unix(1, 0))
+	return vfsgen.Generate(fs, vfsgen.Options{
+		Filename:     fileName,
+		PackageName:  packageName,
+		VariableName: variableName,
+	})
 }
 
 // Extract recursively extracts the assets at src into the directory dst. If dst doesn't exist, the
@@ -77,8 +85,8 @@ func get() assetsIface {
 // The assets are read either from data embedded in the binary or from the filesystem, depending on
 // whether the LOKOCTL_USE_FS_ASSETS environment variable is set.
 func Extract(src, dst string) error {
-	walk := copyingWalker(dst, 0700)
-	if err := get().WalkFiles(src, walk); err != nil {
+	walk := CopyingWalker(dst, 0755)
+	if err := Assets.WalkFiles(src, walk); err != nil {
 		return fmt.Errorf("failed to walk assets: %v", err)
 	}
 
diff --git a/pkg/assets/assets_generate.go b/pkg/assets/assets_generate.go
index ee358481..69797bd7 100644
--- a/pkg/assets/assets_generate.go
+++ b/pkg/assets/assets_generate.go
@@ -18,36 +18,19 @@
 package main
 
 import (
-	"net/http"
-	"time"
-
-	// External dependencies should also be added to assets.go file.
-	"github.com/prometheus/alertmanager/pkg/modtimevfs"
-	"github.com/shurcooL/httpfs/union"
-	"github.com/shurcooL/vfsgen"
+	"log"
 
 	"github.com/kinvolk/lokomotive/pkg/assets"
 )
 
 func main() {
-	directoriesToEmbed := map[string]http.FileSystem{
-		assets.TerraformModulesSource: http.Dir("../../assets/terraform-modules"),
-		assets.ControlPlaneSource:     http.Dir("../../assets/charts/control-plane"),
-		assets.ComponentsSource:       http.Dir("../../assets/charts/components"),
+	dirs := map[string]string{
+		assets.TerraformModulesSource: "../../assets/terraform-modules",
+		assets.ControlPlaneSource:     "../../assets/charts/control-plane",
+		assets.ComponentsSource:       "../../assets/charts/components",
 	}
-
-	// Reset modification time on files, so after cloning the repository all
-	// assets don't get modified.
-	u := union.New(directoriesToEmbed)
-	fs := modtimevfs.New(u, time.Unix(1, 0))
-
-	options := vfsgen.Options{
-		Filename:     "generated_assets.go",
-		PackageName:  "assets",
-		VariableName: "vfsgenAssets",
-	}
-
-	if err := vfsgen.Generate(fs, options); err != nil {
-		panic(err)
+	err := assets.Generate("generated_assets.go", "assets", "vfsgenAssets", dirs)
+	if err != nil {
+		log.Fatalln(err)
 	}
 }
diff --git a/pkg/assets/embedded.go b/pkg/assets/embedded.go
index f0e60334..6efcda12 100644
--- a/pkg/assets/embedded.go
+++ b/pkg/assets/embedded.go
@@ -29,13 +29,15 @@ type embeddedAssets struct {
 	fs http.FileSystem
 }
 
+var _ AssetsIface = &embeddedAssets{}
+
 func newEmbeddedAssets() *embeddedAssets {
 	return &embeddedAssets{
 		fs: vfsgenAssets,
 	}
 }
 
-func (a *embeddedAssets) WalkFiles(location string, cb walkFunc) error {
+func (a *embeddedAssets) WalkFiles(location string, cb WalkFunc) error {
 	return vfsutil.WalkFiles(a.fs, location, func(filePath string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
 		if err != nil {
 			return cb(filePath, fileInfo, r, err)
diff --git a/pkg/assets/fs.go b/pkg/assets/fs.go
index 3162b7ec..d5e917b2 100644
--- a/pkg/assets/fs.go
+++ b/pkg/assets/fs.go
@@ -26,6 +26,8 @@ type fsAssets struct {
 	assetsDir string
 }
 
+var _ AssetsIface = &fsAssets{}
+
 func newFsAssets(dir string) *fsAssets {
 	if dir == "" {
 		execDir, err := osext.ExecutableFolder()
@@ -39,7 +41,7 @@ func newFsAssets(dir string) *fsAssets {
 	}
 }
 
-func (a *fsAssets) WalkFiles(location string, cb walkFunc) error {
+func (a *fsAssets) WalkFiles(location string, cb WalkFunc) error {
 	relativeLocation := strings.TrimLeft(location, string(os.PathSeparator))
 	assetsLocation := filepath.Join(a.assetsDir, relativeLocation)
 	return filepath.Walk(assetsLocation, func(path string, info os.FileInfo, err error) error {
diff --git a/pkg/assets/generated_assets.go b/pkg/assets/generated_assets.go
index bd2c5751..ccb4bbeb 100644
--- a/pkg/assets/generated_assets.go
+++ b/pkg/assets/generated_assets.go
@@ -1086,6 +1086,63 @@ var vfsgenAssets = func() http.FileSystem {
 
 			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x34\xcd\x4b\x0a\xc2\x40\x10\x84\xe1\x7d\x9f\xc2\x73\xf4\x56\x17\x06\x1f\x0b\x83\x07\x18\xc6\x22\x36\xc6\x1e\xa9\xee\x11\xf4\xf4\x42\x82\xbb\xaf\xa0\xe0\xaf\x73\x8f\x04\xcf\xe5\x09\xdd\x88\xf9\x44\x44\xec\x5b\xa4\x4a\x20\xc2\x9a\x1f\xf0\x51\x29\x2f\x1b\xc1\x37\x78\xbd\x1c\x55\x4a\xcf\x7b\xa3\x7d\xb1\xac\x6c\x0f\xf8\xa2\x3a\x1b\x3c\x87\xdd\x5f\x23\x2a\x91\x2a\xc4\xcd\x88\x9a\xeb\x09\xcc\x53\xf1\x32\x81\xdb\x35\x3e\x44\x74\x50\xe5\x17\x00\x00\xff\xff\x68\xbe\xdb\x11\x8c\x00\x00\x00"),
 		},
+		"/charts/components/headlamp": &vfsgen۰DirInfo{
+			name:    "headlamp",
+			modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+		},
+		"/charts/components/headlamp/.helmignore": &vfsgen۰CompressedFileInfo{
+			name:             ".helmignore",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 349,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\x8e\x41\x6e\xe3\x30\x0c\x45\xf7\x3c\xc5\x1f\x78\x33\x63\x0c\xe4\x43\x24\xb3\x98\x55\x0b\xa4\xc8\xb6\x90\x6d\x46\x62\x22\x8b\x82\x44\x27\x6d\x17\x3d\x7b\x91\x04\x41\xbb\x79\x20\x3f\xc8\x8f\xd7\xe1\xd9\x9b\x71\xcd\x0d\xa6\x90\x90\xb5\x32\x2e\x91\x33\xc6\x55\xd2\x2c\x39\xa0\xf8\xe9\xe4\x03\x37\x47\x1d\x5e\xa2\x34\xb4\xb5\x14\xad\xd6\xd0\x22\xa7\x84\x90\x74\xc4\xe2\x6d\x8a\x92\xc3\x5f\x54\x4e\xde\xe4\xcc\x28\xde\xe2\x8f\xdc\xe7\x99\x3a\x64\x0e\xde\x44\x33\x7e\x97\xca\x07\x79\xe3\x19\x17\xb1\x88\x5f\x7f\x1c\x9e\x72\x7a\x87\xe6\xdb\xe7\x55\x09\x85\x2b\x92\x64\x76\xe4\xb6\xbb\xd7\x9d\x69\x65\xea\xb0\xd1\x65\xd1\x8c\xfd\x66\x87\x59\x6a\x23\x17\xc4\x86\x1b\xef\xfa\xe4\xc6\x8f\x3a\xdc\xf8\x08\x62\x18\xae\x78\xac\xed\x9c\x87\xef\xa2\xd1\x4f\xa7\xb5\xe0\x20\x89\x1b\xf5\xae\x5d\x0a\xf5\x6e\xf4\x27\xea\x9d\x2d\xd7\x59\xab\x04\xea\x3f\xa9\xc3\xde\x57\xd1\xb5\xe1\xff\xf6\x5f\x23\x57\xaa\x1e\x79\x32\x72\x32\xb3\x1f\xee\xe7\x55\x8f\xe4\xce\x6d\xd2\x99\x07\xfa\x0a\x00\x00\xff\xff\x16\xec\x32\x27\x5d\x01\x00\x00"),
+		},
+		"/charts/components/headlamp/Chart.yaml": &vfsgen۰CompressedFileInfo{
+			name:             "Chart.yaml",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 176,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x44\x8e\x3d\xae\xc3\x20\x10\x06\x7b\x4e\xf1\x5d\xc0\x88\xf7\x4a\x77\xa9\xdd\xbb\x5f\xcc\x26\x46\xc1\xec\x8a\x3f\x89\xdb\x47\x89\x14\xa5\x9d\x29\x66\x48\xe3\xce\xa5\x46\xc9\x2b\xc6\xbf\xc9\x74\xf1\x8a\x93\x29\x24\xba\xd4\x04\xae\x47\x89\xda\x3e\xfa\x96\xc1\x54\xe7\xd2\x64\xe9\x95\x41\x39\x60\x70\xa9\xd4\x62\x62\x04\xaa\xa7\x17\x2a\x01\x77\x29\xd8\xba\xe7\x92\xb9\x71\x85\x2f\xd2\x1f\x67\x43\x13\x4c\xe9\xf0\x13\x5b\xcc\x43\xd2\xd3\x9a\x36\x95\x57\x90\x6a\x8a\x07\xbd\x1b\xc6\x8c\xef\x8b\xb3\x7f\xd6\x19\x52\xdd\x7f\xc4\x59\x67\x5e\x01\x00\x00\xff\xff\x83\x30\x02\xa9\xb0\x00\x00\x00"),
+		},
+		"/charts/components/headlamp/templates": &vfsgen۰DirInfo{
+			name:    "templates",
+			modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+		},
+		"/charts/components/headlamp/templates/_helpers.tpl": &vfsgen۰CompressedFileInfo{
+			name:             "_helpers.tpl",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 1792,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x94\x55\x4d\x6b\xdb\x40\x10\xbd\xeb\x57\x0c\x4b\x03\x49\x8a\xd6\x87\x42\x0f\x81\x1e\x42\xda\x43\x69\x49\xa1\x86\xf4\x58\x56\xab\x51\xb4\x74\xb5\x52\xf6\xc3\x8d\x91\xfd\xdf\xcb\x7e\x58\x96\x65\xd9\x75\x6e\x8b\xf5\xf6\xcd\x7b\x6f\x66\xd6\x7d\xbf\xb8\xcd\xbe\xbc\x76\x4c\x95\x60\x6b\x04\xc5\x1a\x84\xb6\x0a\x67\x5e\x33\x6d\x69\x76\xbb\xd8\x6e\xb3\xbe\xcf\xa1\xc4\x4a\x28\x04\x52\x23\x2b\x25\x6b\x3a\xea\xc1\x04\xf2\xfd\x67\xe6\xa4\x05\xfa\x10\xee\x3d\x7a\x26\xfa\xc4\xa4\x43\x13\x90\x3f\x56\xa8\xb5\x28\x11\x36\x60\xb5\x53\x1c\x3e\x7e\x08\x47\xd1\x2c\x5d\x55\x89\x57\x20\x39\x81\xc4\x85\xaa\xf4\xc7\x2c\xc8\x7b\xd0\xc8\x2c\x02\x1b\x2a\x54\x4e\xca\x35\xbc\x38\x26\x45\x25\xb0\x04\xd6\x75\x41\x38\xcd\x7e\x61\xe4\x0e\x78\xeb\x2b\x78\x13\x06\x0a\xe4\xcc\x19\x04\xd3\x36\x08\xdf\x5c\x81\x5a\xa1\x45\x13\xed\x56\x02\x65\x69\x80\x69\x04\x29\x1a\x61\xb1\x04\xdb\x82\xad\x85\x81\xeb\x62\x1d\xa2\xf8\xfc\xb8\xf4\x58\xa1\x9e\xc1\x74\xc8\x6f\x68\xf6\xb5\x02\x8d\x12\x99\x49\x99\xf1\x56\x59\x26\x94\x89\xa9\xc5\xdf\x84\x85\xbf\x42\x4a\x28\x10\x9c\xf1\x3a\x0d\xb0\x20\x3e\xa9\x3d\x9d\xac\x07\x1d\xa6\x2b\xaa\x21\xcc\xdd\xc7\x21\xd0\x04\x39\xf9\xfd\x92\xc0\xa5\x19\x78\xde\x05\xf1\x77\x9f\x2e\xef\xe8\x5e\xe3\x10\x43\x24\xa1\x3f\x63\x46\xf1\xee\x4e\xe7\xc1\x8f\x6f\x14\xd7\x69\xa1\x6c\x05\xe4\xca\xe4\x57\x86\x4c\xb8\x62\xd1\xcb\xe7\x6b\xfe\x78\x30\x75\xa3\x76\xfa\x1d\x59\xa1\x36\xa2\x55\xbe\x95\xa1\xa5\x69\x3e\x22\x4a\xb2\x02\xe5\xb9\xb6\x06\xd8\xbe\xa7\x53\x2f\xe3\x98\xe3\xf9\x29\x95\xdb\x80\xc6\x4e\x32\x8e\x40\xde\x13\x20\xbf\xc9\xdb\x97\xa8\x6d\x9a\x56\x45\x89\xe6\x8c\xc4\x08\x88\x1a\x6b\x94\x0d\x35\xf5\x22\xc8\xbe\x83\xbe\x07\xa1\xb8\x74\xe5\x8c\x25\x1a\x8b\xce\x00\x0c\x4a\xe4\xb6\xd5\xdf\x13\x31\x1d\x4d\x4b\x72\x79\xdf\x75\x3b\xa3\xdb\x6d\xc6\xba\x8e\xfe\x19\x56\x94\x8a\x76\x91\x42\x0f\x0a\x8e\xaf\x6c\xe0\xc5\xb5\x16\x27\xae\x8f\x59\x1a\xa6\xd8\x33\x96\x79\xb1\x8e\x44\xbb\xc9\x59\xa2\x5e\x09\x3e\xbd\x1f\x53\x5b\x26\xf1\xff\xcf\x6d\x6a\x33\x9f\xd5\xe0\xe7\xe8\x44\x90\x71\xdd\xe9\xbc\x76\xa1\x8c\x65\x8a\xe3\xa1\xf2\xf1\x52\xcd\xcd\xee\xf4\x41\x37\xc9\x29\xe3\xbc\x75\xca\xfa\x57\xce\x19\x3c\x6b\x2a\x5c\xb8\x8f\xf8\xc7\x53\x0f\xd2\x21\x8c\xf2\x58\x7d\xf2\xb7\x70\x7d\x6c\x79\xff\xca\xd1\x9b\x53\x64\x6a\xec\x71\xf4\x0e\xec\x68\x49\x3a\x90\x8b\x08\x8e\x76\xfd\x5f\x00\x00\x00\xff\xff\x9f\x46\x7b\x85\x00\x07\x00\x00"),
+		},
+		"/charts/components/headlamp/templates/deployment.yaml": &vfsgen۰CompressedFileInfo{
+			name:             "deployment.yaml",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 1839,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xc4\x54\x4d\x8b\xdb\x3c\x10\xbe\xe7\x57\x0c\xb9\xdb\x61\x5f\x5e\x4a\x31\xf4\xb0\xa4\xd0\xcb\x52\x02\x29\x0b\x3d\xce\x4a\x93\x58\x54\x96\x84\x34\x4e\x1b\xb2\xfe\xef\x45\xeb\x38\x96\x1c\xa7\x1f\xdb\x42\x7d\x9c\xd1\xf3\x35\x1a\x0b\x9d\x7a\x24\x1f\x94\x35\x15\xa0\x73\x61\x75\xb8\x5b\x7c\x51\x46\x56\xf0\x9e\x9c\xb6\xc7\x86\x0c\x2f\x1a\x62\x94\xc8\x58\x2d\x00\x0c\x36\x54\xc1\xe9\x04\xca\x08\xdd\x4a\x82\x65\x4d\x28\x35\x36\xae\xdc\xb5\x5a\xc7\xf6\x12\x4a\xe8\xba\x05\x80\xc6\x27\xd2\x21\xa2\x00\x4e\xa7\x62\x06\xd2\x9f\x88\x80\x67\x30\xca\x48\x32\x0c\xff\x47\x70\x70\x24\x22\x30\x90\x26\xc1\xd6\xf7\x24\x0d\xb2\xa8\x1f\x12\xd6\x5b\xbc\x03\xec\x61\x86\xff\x4d\x6f\x8e\xa9\x71\x1a\x99\xce\xcc\x49\xc4\x9e\xf4\xab\xe2\x1a\xca\x47\xd4\x2d\x85\xd2\x59\x79\x6f\x8c\x65\x64\x65\x4d\xe8\x09\xe2\x87\x63\x71\x30\xd4\xa3\xd9\x7e\xc6\x46\x67\xba\x6f\x07\x58\xec\x93\x91\x23\x8b\xce\x12\xbd\x26\xd3\x85\x7b\x98\xdb\x6c\x0a\xd5\xe0\x9e\x36\xad\xd6\x5b\x12\x9e\x38\xc9\x31\xed\xfc\x7a\x98\x99\x38\x81\x44\xeb\x15\x1f\xd7\xd6\x30\x7d\xe3\x79\xae\x71\xb0\xdb\xfc\xf8\xbc\x88\xb0\x86\x51\x19\xf2\x89\xb5\x62\x5c\xc6\x72\x5d\xa3\xe7\xf2\x23\x36\x34\x62\x7e\xe8\xe5\x86\x9f\x70\xd3\xcc\xdd\x7f\x39\xf3\xcb\xc8\x2a\x58\x46\xf5\x74\xbe\xa5\x27\x67\x83\x62\xeb\x8f\xd0\x75\xd5\x55\x9b\x71\x0f\xcf\x20\x69\x87\xad\xe6\xc1\xf8\xbd\x73\xe7\xdf\x10\xba\x6e\x39\x55\x89\x17\xb3\xb1\x5a\x89\x63\x1f\x36\xe3\x73\x97\x66\xee\x0f\xfd\x3e\xa4\x71\x0b\x28\x94\x29\x84\x6e\x03\x93\xcf\xeb\x4e\xb7\x7b\x65\x42\x21\x95\x7f\xb7\x1a\xf6\x6d\x75\xae\xae\x92\xb3\xce\x7a\x0e\xf9\x0c\x87\x4b\xa8\x99\x5d\xd6\x48\xee\x6c\x63\x3d\x67\xce\x03\xf9\x83\x12\x71\x16\x7e\x4f\x1c\xdb\xb9\xf7\x17\x2d\x6f\xd9\x0a\xab\x2b\xf8\xb4\xde\x24\x3d\xad\x0e\x64\x28\x84\x8d\xb7\x4f\x94\x7b\x89\x1e\x3e\xd0\xe4\x92\x01\x1c\x72\x5d\xc1\x6a\x5a\xfd\x5d\x53\x9e\x50\xaa\x7f\xa4\x1c\x6c\xeb\x05\x85\x9f\xae\xef\xe5\xe4\x8d\xc5\xbd\x7a\x12\x8c\x95\xb4\x3d\x3f\x2c\xe3\xb1\xb4\xfa\x47\x4f\xc1\x95\x1e\xee\x76\xca\x28\x4e\x76\x75\xa8\xfc\x5d\x1d\xb6\x9a\xfc\xf4\xb5\x4e\x8a\xaf\x54\xfb\x1e\x00\x00\xff\xff\xa9\xad\x86\xd3\x2f\x07\x00\x00"),
+		},
+		"/charts/components/headlamp/templates/ingress.yaml": &vfsgen۰CompressedFileInfo{
+			name:             "ingress.yaml",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 1054,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x93\xc1\x8a\xdb\x40\x0c\x86\xef\x7e\x0a\x11\x7a\xcd\xb4\x81\x3d\x14\xc3\xf6\xb2\x87\x52\x0a\xa1\xa7\x85\x1e\x65\x5b\x89\x45\xc6\x1a\x77\x46\xce\x16\x5c\xbf\x7b\x19\x8f\x4d\xec\x4d\xcc\xde\x66\xa4\xef\x97\xf4\xcb\x9e\xbe\xdf\x03\x9f\xc0\xbc\xa2\xed\x28\x18\x96\xb3\xa7\x10\x0c\x09\x16\x96\x2a\xd8\x0f\x43\x16\x91\x4f\xa7\xce\xda\x23\x36\x04\xf9\x33\xb0\x94\xb6\xab\x08\x76\x35\x61\x65\xb1\x69\x4d\xcc\x0a\x36\xb4\x03\x73\x93\x84\x6b\xf9\xcb\x79\x8d\x8a\xb9\x7c\x20\x7f\xe5\x92\x4c\x1b\xe3\x33\xc8\x27\x08\xd4\x5c\xc9\xbf\xb8\xa6\x45\x4f\xb0\xfb\xf6\x7c\x30\x87\xa7\xfd\x97\x1d\x98\x17\x6c\xb1\x60\xcb\xca\x14\xcc\xcf\xae\xa0\x57\xf2\x81\x9d\x98\xef\xac\xd3\x71\x2c\x84\x2d\x4f\xd7\x1c\x84\xf4\xcd\xf9\x0b\xcb\xd9\x5c\xbe\x06\xc3\xee\xf3\xf5\x50\x90\xe2\x61\x6c\x47\x36\xd0\x9d\x84\xfe\x2a\x49\x3c\x86\x35\x2b\x15\x0c\x43\x76\x61\xa9\x72\xf8\x91\x76\x93\x35\xa4\x58\xa1\x62\x9e\x01\x44\xd3\x39\xf4\xfd\x62\x41\xc3\x90\x01\x58\x2c\xc8\x86\x48\x00\x8c\x1e\xef\x56\x96\x88\xb8\xb0\x7f\x20\x2c\x15\x89\xc2\x53\x12\x47\xc1\x1b\x6b\x7d\xf7\x55\x50\xc4\x29\x6a\x1c\x33\x91\x8b\xc0\xad\x97\xba\xdf\xd8\xd8\xad\xc2\x93\xa5\xd0\x52\x99\x4f\xa1\x07\xdf\x5f\xed\xd4\x41\x97\x2e\x3c\xca\x99\xb6\x59\x80\x3d\xd4\x2e\xe8\xa4\x78\xa7\x1a\x33\x33\x98\xe0\xbe\x1f\xa7\xfc\xd3\x39\xa5\x65\x66\x31\x67\x0a\x04\x2a\x3d\xe9\x71\x5e\xb6\xb9\xdd\x67\x66\x25\x59\x5d\x7c\x67\xe9\x43\x0f\xab\xe1\x92\x8b\xd4\x29\x9e\xee\x47\xac\x55\xdb\x9b\xc9\x16\xb5\x5e\x78\x5e\xf5\x19\x73\x4b\x6f\xb1\x7c\x0c\xa6\xf2\xeb\x0c\x40\x81\xe5\x85\xa4\xca\x57\xc1\xe8\x7f\x7c\x37\xc7\x8d\xbf\xed\x01\x1a\x5f\x5e\x42\xe7\x67\xb8\x22\xdf\x6d\x78\x6b\x7b\xff\x03\x00\x00\xff\xff\x36\xe9\xb3\x7e\x1e\x04\x00\x00"),
+		},
+		"/charts/components/headlamp/templates/service.yaml": &vfsgen۰CompressedFileInfo{
+			name:             "service.yaml",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 392,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x7c\x90\xc1\x4e\x43\x21\x10\x45\xf7\x7c\xc5\x4d\xf7\x25\x31\x71\xc5\xd6\xad\x8b\x26\x9a\xee\x47\x18\x2d\x71\x1e\x10\x98\xd7\xc4\x60\xff\xdd\x20\x35\xdd\xf4\xc9\x0e\xee\x39\xb9\xcc\x50\x89\x47\xae\x2d\xe6\xe4\x70\x7e\x30\x9f\x31\x05\x87\x17\xae\xe7\xe8\xd9\x2c\xac\x14\x48\xc9\x19\x20\xd1\xc2\x0e\xbd\x23\x26\x2f\x6b\x60\xec\x4e\x4c\x41\x68\x29\xf6\x7d\x15\x19\xf1\x0e\x16\x97\x8b\x01\x84\xde\x58\xda\xb0\x80\xde\xf7\x77\x94\x49\x0c\xe1\x1b\x29\xa6\xc0\x49\xf1\x38\xe4\x56\xd8\x0f\x51\xbf\xca\xac\xb3\x47\x92\x95\x9b\x6d\xf3\x4f\x76\x04\xb3\xa5\xe4\xaa\xd7\x92\xfd\xef\xe5\x2e\x3f\x82\xc9\x8f\xa3\x54\x3f\x58\x0f\x5b\xf0\x2d\xbe\x29\xa5\x66\xcd\x3e\x8b\xc3\xeb\xd3\xe1\xfa\x36\x97\x71\x52\x2d\x06\x68\x2c\xec\x35\xd7\xff\xe6\xfd\x63\x9e\xb7\xe6\xfe\x09\x00\x00\xff\xff\x25\x3b\x95\xee\x88\x01\x00\x00"),
+		},
+		"/charts/components/headlamp/values.yaml": &vfsgen۰CompressedFileInfo{
+			name:             "values.yaml",
+			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			uncompressedSize: 1402,
+
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x54\x4d\x6f\x1b\x47\x0c\xbd\xcf\xaf\x20\xe2\x43\x2e\x91\x6c\x07\x41\x60\xec\xcd\x70\xd0\xc2\x80\xed\x18\x75\x3f\x50\x04\x39\xd0\xb3\x5c\x69\x6a\xee\x70\x43\x72\x64\x2f\x8a\xfe\xf7\x62\x66\x25\xbb\x49\x53\xa0\x27\xcd\xf2\xeb\x3d\xf2\x91\x3a\x82\x0f\x34\x60\x61\x87\x1d\x72\x21\x83\x41\x14\xb6\x84\x3d\xe3\x38\xad\xc3\x11\xfc\xbc\x4d\x06\xc9\x00\xe1\xf7\xf3\xeb\xab\xd5\x20\x3a\xa2\x3b\xf5\x30\x24\xa6\x1a\xf0\x81\x22\xa3\x12\xec\x50\x13\xde\x33\x19\xb8\xc0\x3d\xc1\x84\x66\xd4\x43\xca\x2e\x30\x4b\x51\x70\x1a\x27\x46\x27\x5b\x87\xa0\x34\x71\x8a\x78\x21\x25\x7b\x07\xa7\x21\xa4\x11\x37\xd4\x05\x00\xa5\x49\x2c\xb9\xe8\xdc\xc1\x97\x82\xf3\x3a\xc9\xf1\x43\xca\x3b\xe1\x87\xe3\x03\xaf\x00\x30\x15\xe6\x5b\xe1\x14\xe7\x0e\x2e\x87\x1b\xf1\x5b\x25\xa3\xec\x01\xe0\x08\x3e\xee\x48\x35\xf5\x95\xca\x96\xa0\x95\x06\xc7\x0d\x3c\x6e\xc5\x08\xfa\x7d\xc3\x69\x71\xc7\x2d\xaa\x03\x4e\xd3\xaf\xa4\x96\x24\xaf\x03\xd4\xe0\x0e\xf8\x8c\xf6\xbc\x6e\x0b\xf3\x1d\x45\x25\xb7\x0e\x3e\x7d\x0e\x19\x47\x3a\x60\x74\xf0\xea\x55\x18\x0a\xf3\xbf\x8c\x61\x92\xfe\x3c\x67\x71\xf4\x24\xd9\x3a\xf8\xf3\xaf\x66\xbb\xa3\x58\x34\xf9\x7c\x21\xd9\xe9\xc9\x9b\xbd\xb2\x1e\xec\x47\x95\x32\x75\xf0\xf6\xe4\xe4\x24\x04\xfb\x8f\xb0\x88\x13\xde\x27\x4e\x9e\xc8\xba\x66\x01\xe8\x55\xa6\xc3\x7b\x05\xe7\x57\x57\xed\xad\x84\xfd\xc7\xcc\xf3\x4f\x22\xfe\x43\x62\xb2\xd9\x9c\xc6\x0e\x5c\x0b\x2d\x01\x25\x9f\xdb\x8d\xe4\x1a\xf0\xad\xf9\x17\x23\xed\xe0\x74\x4f\x45\x77\x29\x36\x79\x7c\x9e\xa8\x83\x0b\x2e\xe6\xa4\x97\xb7\x55\x09\x51\xef\xe0\xec\xa4\x8d\x4d\x37\xe4\xb7\xcd\xf0\xee\xdd\xfb\xf7\x21\xa4\xbc\x51\xb2\xc6\x93\x72\xdd\x8e\xbe\x83\x01\xd9\x2a\x10\x7e\x33\x1b\x80\x0a\xfe\x50\xee\x49\x33\xd5\x35\x49\x72\xbc\xcf\x5f\x47\x46\xb3\x0e\xf2\x26\xe5\xa7\xef\x06\x3a\xdb\x0a\xe3\x58\x27\x5f\xfb\x78\x15\x00\xb6\x62\xde\x90\xeb\x4c\xea\x47\xb7\x68\xbd\xa2\x27\x1c\x27\xa6\x35\x4b\x44\x6e\x7e\x80\x09\x7d\xbb\x88\x0b\xe0\x7c\x78\x1d\xd5\x54\x6b\xca\xdf\x60\x2d\xfe\x55\x81\x95\xb3\xed\x87\xfe\x02\x76\xb4\xd4\x5b\x7d\x17\x2b\x28\x99\x14\x8d\x64\xcf\x72\xfe\x46\x50\xac\x20\xf3\x0c\x4a\x51\xc6\x91\x72\x0f\x59\xbc\x5e\x91\x4d\x14\xd3\x30\x3f\x6f\xec\x73\x36\x60\xee\x6b\x00\x13\xee\x08\xbc\x9e\x28\xd6\x13\x8d\x92\x2d\x26\x29\x0b\xad\xb8\x95\x14\xa9\x5d\x74\x5d\xf4\x62\xa4\xeb\xe5\x9e\x91\x4d\x20\xe5\xa8\x84\x46\x56\xa9\xe6\xb8\xfc\xaa\x5b\xd5\x1f\x24\x03\xe5\x5d\x52\xc9\x23\x65\x37\x78\x4c\xbe\x05\x4e\xee\xbc\xdf\x91\x03\x95\x37\x60\x25\x6e\x2b\xfc\x75\xca\xa9\x8a\xb2\x86\xcb\xa1\x5e\x3c\xf4\x02\x8f\x98\xbf\xea\xe4\x1f\x69\x25\x2f\xdd\x7a\x23\x37\x08\xb3\x3c\xa6\xbc\x69\xd5\x39\xe5\x1a\x82\xfd\x1f\xc5\x9a\x7f\xac\x00\x99\x22\x99\xa1\xce\x6f\x5a\xff\x4a\xa3\xb4\xee\x09\x62\x51\x9e\xe1\x5e\xb1\xcd\x66\x70\x52\x78\xfd\x32\xea\xd7\xeb\x7d\xd1\x31\xbd\x68\x14\xa7\xd2\xd6\x7b\xdc\x7f\x8f\x34\xb6\xff\x9c\xd3\xb7\x67\xd7\x69\xdf\xe2\x97\x42\xf6\x7f\x33\x42\x96\x9e\xee\x88\x29\xba\xe8\x72\xeb\x2e\x4c\x7a\x58\xf0\x4f\x9f\x43\xc0\x61\x48\x39\xf9\xdc\xdc\x7f\x07\x00\x00\xff\xff\xaf\xd0\x24\xe6\x7a\x05\x00\x00"),
+		},
 		"/charts/components/httpbin": &vfsgen۰DirInfo{
 			name:    "httpbin",
 			modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
@@ -5124,9 +5181,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/aws/flatcar-linux/kubernetes/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 499,
+			uncompressedSize: 208,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\xd0\xc1\x6a\xc4\x20\x10\x06\xe0\xbb\x4f\x31\xa4\x77\x37\xd9\x40\x6f\xe9\x53\xf4\x5e\x44\xa7\x5d\xc1\x38\x76\x46\x77\x0f\x25\xef\x5e\x76\x93\x14\x0b\x42\xf6\x22\x32\xf3\xff\x1f\xe2\x0b\xbc\x23\xb3\xf9\x24\x9e\xe1\x8a\x2c\x9e\x22\x98\xe8\x20\x85\xf2\xe5\xe3\x3e\x12\xa5\xf2\x5f\xec\x47\x01\x30\x7e\x17\xcf\xe8\x3e\xf6\xce\x04\xdd\xdb\x04\xbd\x1e\xc6\x4e\xd5\xfb\xc4\x74\xf5\x0e\x59\x1e\x35\x00\x73\x13\x98\xb6\x3b\x80\x50\x61\x8b\x70\x6f\x5f\x8c\x5c\xbc\x25\x4e\x27\x73\x93\x6e\xdb\x57\xfa\xa8\x47\xdd\xaf\xf3\xe5\x71\xda\xdc\x86\x12\x09\x7a\x47\xf1\x64\x73\x83\xe9\xf5\xab\x1e\x6a\x26\x96\x10\x8e\x5e\x74\xcf\x34\xac\xb3\x1e\xf4\xb9\xb6\x32\xce\x29\x98\x8c\x47\xde\x9e\x7b\xc6\x64\x13\x1d\xcd\x47\xe2\x9a\x6a\x7a\xff\xbe\x6d\x51\x8b\xfa\x0d\x00\x00\xff\xff\xfe\xaa\xfc\x3d\xf3\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcb\x4d\x0a\xc2\x30\x10\xc5\xf1\xfd\x9c\xe2\x51\xf7\x43\x3f\xc0\x5d\x3d\x85\x7b\x09\x76\x94\x40\x9a\xc6\x49\x52\x17\xd2\xbb\x4b\x5b\x52\x74\x76\xf3\xe3\xff\x4e\xb8\x8a\xaa\x79\x4c\x3a\x62\x16\x8d\x76\xf2\x30\x7e\x40\x70\xf9\x69\x7d\xa1\x48\x94\x8e\xec\x43\x80\xca\x2b\x5b\x95\xe1\x56\x36\x3d\xaa\x4b\x8f\x9a\x9b\x96\xeb\x8a\x7e\x8b\xa0\xd3\x6c\x07\xd1\xb8\x0d\x01\xf3\x8e\xd8\xae\x47\xd5\x71\xb7\xe6\xeb\x77\x4f\x38\xb8\xe6\x33\x37\x3b\xfb\xec\x5c\xe1\x96\x1b\x6e\x77\x4e\x32\x06\x67\x92\xfc\xf1\x42\x0b\x7d\x03\x00\x00\xff\xff\xe6\xfc\x25\x90\xd0\x00\x00\x00"),
 		},
 		"/terraform-modules/aws/flatcar-linux/kubernetes/workers": &vfsgen۰DirInfo{
 			name:    "workers",
@@ -5188,9 +5245,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/aws/flatcar-linux/kubernetes/workers/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 423,
+			uncompressedSize: 185,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\xcf\xcb\x6a\xc5\x20\x10\x06\xe0\xbd\x4f\x31\xa4\x7b\x4f\x2e\xd0\x5d\xfa\x14\xdd\x17\xd1\x69\x23\x24\x8e\x9d\x31\xc9\xa2\xe4\xdd\x4b\x73\x29\x1e\x10\xb2\x11\x99\xf9\xff\x0f\x7d\x81\x77\x64\x36\x9f\xc4\x13\x2c\xc8\xe2\x29\x80\x09\x0e\xe2\x38\x7f\xf9\x70\x8d\x44\xa9\xf4\x1f\xfb\x51\x00\x8c\xdf\xb3\x67\x74\x1f\x57\xa7\x87\xea\xad\x87\x5a\x37\x5d\xa5\xf2\x7d\x64\x5a\xbc\x43\x96\xbd\x06\x60\x56\x81\xfe\xbc\x03\x08\xcd\x6c\x11\xfe\xda\x83\x91\xc1\x5b\xe2\xf8\x30\xab\x54\xe7\x3e\xd3\x3b\xdd\xe9\xfa\x98\x6f\xfb\x69\x53\x19\x8a\x24\xe8\x1d\x85\x87\x4d\x05\xa6\xd6\xaf\xba\xc9\x99\x84\x53\x1c\x4d\xc2\xbb\x57\x5d\xb9\x82\xd9\xea\x46\xb7\xb9\xc9\x26\x38\x9a\xee\xc4\x23\x55\xf4\x9e\xbe\xba\xa9\x4d\xfd\x06\x00\x00\xff\xff\x9e\x7e\x5c\xf9\xa7\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\xcb\x4d\x0a\xc2\x30\x10\xc5\xf1\xfd\x9c\xe2\x51\xf7\x43\x3f\xc0\x5d\x3c\x85\x7b\x09\x66\x94\x40\x9b\xc4\x49\x5a\x17\xd2\xbb\x4b\x2a\x2d\xce\x6e\x7e\xfc\xdf\x09\x57\x51\xb5\x8f\xa8\x13\x16\xd1\xec\x63\x80\x0d\x0e\x69\x9c\x9f\x3e\xec\x94\x89\xca\x91\x7d\x08\x50\x79\xcd\x5e\xc5\xdd\xf6\x8d\x41\x73\x31\x68\xb9\xeb\xb9\x6d\xe8\xbf\x48\x1a\x17\xef\x44\xf3\x36\x04\xec\x3b\x63\x3b\x83\x66\xe0\xa1\xe6\xf5\xbb\x17\x1c\xdc\xf2\x99\xbb\x1f\x17\x99\xd2\x68\x8b\x54\xee\xb9\xe3\xbe\xf2\x4a\x2b\x7d\x03\x00\x00\xff\xff\xed\xd8\x31\xb6\xb9\x00\x00\x00"),
 		},
 		"/terraform-modules/aws/flatcar-linux/kubernetes/workers/workers.tf": &vfsgen۰CompressedFileInfo{
 			name:             "workers.tf",
@@ -5288,9 +5345,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/azure/flatcar-linux/kubernetes/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 427,
+			uncompressedSize: 209,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\xcc\xbd\x0e\xc2\x20\x10\xc0\xf1\x9d\xa7\xb8\xd4\x9d\x7e\x45\xb7\xfa\x14\xee\x86\xc0\x69\x49\x28\xe0\x01\x1d\x34\x7d\x77\xd3\xda\x9a\x1a\x9b\x74\x21\xe4\xee\x7f\xbf\x03\x5c\x90\x48\xdc\x1c\x75\xd0\x23\x05\xed\x2c\x08\xab\xc0\x9b\x74\xd7\x76\x19\x05\xc6\xe2\x37\x7b\x31\x00\xc2\x47\xd2\x84\xea\xba\xdc\x34\x90\x9d\x1b\x28\x78\x59\x67\x6c\xbd\xf7\xe4\x7a\xad\x90\xc2\x74\x06\x20\x23\x34\xf3\x17\x20\xb8\x44\x12\xc7\x5b\xef\x02\x6a\xe5\x6c\x2e\x63\x36\x2f\x57\x72\xc1\x4f\xbc\xf8\xcc\x87\xe9\x15\xcf\x44\x48\xdd\x3f\x35\xd6\xad\x08\xad\x96\x8e\x7c\x3e\x67\x1b\x62\xc9\xeb\xe3\x2f\x69\x93\x31\x7b\xde\xd8\x6c\x60\x15\x2f\x79\xb5\xb6\x22\x76\xde\x88\x88\x7b\xde\xd2\xed\x9b\x03\x1b\xd8\x3b\x00\x00\xff\xff\xc4\x04\xfd\x27\xab\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcb\xc1\xaa\xc2\x30\x10\x85\xe1\xfd\x3c\xc5\xa1\x77\x3f\xb4\xbd\xe8\xae\x3e\x85\x7b\x09\x76\x94\x40\x9a\xc4\x49\xd2\x85\xd2\x77\x97\xb6\x44\x74\x76\xf3\xf1\x9f\x3f\x9c\x45\xd5\xdc\x82\x4e\x98\x45\x93\x0d\x1e\xc6\x8f\x88\xae\xdc\xad\xaf\x94\x88\xf2\x27\x7b\x11\xa0\xf2\x28\x56\x65\xbc\xd4\xcd\x80\xe6\x34\xa0\xe5\xae\xe7\xb6\xa1\xef\x22\x6a\x98\xed\x28\x9a\xb6\x21\x70\xcd\xd8\x6f\x40\xd3\xf2\x71\xcd\xd7\xcf\x3c\x8b\x8a\x4e\x1b\x77\xfc\x7f\xa8\xee\x8b\x73\x35\xef\xb9\xe3\x7e\xe7\x2c\x53\x74\x26\xcb\x0f\x2f\xb4\xd0\x3b\x00\x00\xff\xff\x46\xfd\x43\x89\xd1\x00\x00\x00"),
 		},
 		"/terraform-modules/azure/flatcar-linux/kubernetes/workers": &vfsgen۰DirInfo{
 			name:    "workers",
@@ -5317,9 +5374,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/azure/flatcar-linux/kubernetes/workers/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 310,
+			uncompressedSize: 145,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\xcc\x4d\x0a\xc2\x30\x10\x05\xe0\x7d\x4e\x31\xf4\x00\xe9\x1f\xba\xab\x57\x91\x90\x8e\x34\x60\x3b\xf1\x25\xed\x42\xe9\xdd\xa5\x35\x62\xc5\x82\x9b\x10\xe6\xbd\xf7\x45\x06\xcc\x45\xd0\xd3\x43\x11\x81\x6f\xa3\x03\xb7\xe7\x89\x11\x9c\x0c\xd4\x50\x76\x6a\xa8\xd0\x65\x9d\xa9\x6d\xee\x21\x93\x6b\x19\x61\x9d\x11\xd9\x48\x4d\xfa\x12\x05\x19\x61\x79\xd9\x7a\x09\xec\x5a\x19\x72\x1b\xb3\x14\x6e\xe4\x42\x1f\x75\xf1\xba\xcf\xeb\x6b\xee\x23\x18\xfd\x2f\xb5\xb4\x3b\x13\x3a\x67\x05\x3e\x4f\xb5\x1d\xb1\xd4\xf5\xe1\x9b\x8c\xdc\xfb\xab\x89\xfc\xcf\x7c\xf7\x76\xd0\x4a\x97\xba\xfa\x98\xb3\x9a\xd5\x33\x00\x00\xff\xff\xc0\x3a\xe4\xec\x36\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\xcb\x41\x0a\xc2\x30\x10\x85\xe1\x7d\x4e\xf1\xc8\x01\x86\x24\xa2\xbb\x78\x15\x09\x76\x84\x80\x31\xf5\x35\xed\x42\xe9\xdd\x25\x11\xc1\xd9\xcd\xc7\xff\x9a\x92\xe9\x56\x59\xf0\x36\x00\xf5\xb9\x66\xea\x74\xd9\x94\x4b\xae\x0f\x44\xd8\x73\x84\x13\x1f\xc4\x59\xf3\x5f\xcc\xac\x5b\x9e\x94\xcb\x18\x02\xd7\x86\xef\x45\x58\x27\xa7\x9e\xf7\x2f\xbd\x56\x2a\xcb\x60\x2f\x87\xe3\xcf\x9b\x96\xf9\x9e\x9a\x76\x0f\xe2\x25\x74\xde\xcd\x6e\x3e\x01\x00\x00\xff\xff\xba\x97\x44\x23\x91\x00\x00\x00"),
 		},
 		"/terraform-modules/azure/flatcar-linux/kubernetes/workers/workers.tf": &vfsgen۰CompressedFileInfo{
 			name:             "workers.tf",
@@ -5438,9 +5495,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/bare-metal/flatcar-linux/kubernetes/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 508,
+			uncompressedSize: 208,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x8f\xc1\x6a\xc4\x20\x10\x86\xef\x3e\xc5\x90\xde\xdd\x64\xb7\xf4\x96\x3e\x45\xef\xc5\xea\xb4\x11\xd4\xb1\xa3\x2e\x85\x92\x77\x2f\x9b\xc4\x36\x05\x21\x7b\x11\x99\xf9\xfe\x8f\xf9\x1f\xe0\x05\x99\xd5\x3b\xb1\x87\x2b\x72\xb2\x14\x40\x05\x03\xd1\x95\x0f\x1b\xea\x28\x09\x91\x7f\xb1\x6f\x01\xc0\xf8\x59\x2c\xa3\x79\xad\x99\x11\xba\xe7\x11\x7a\x39\x5c\x3a\xb1\xdf\x47\xa6\xab\x35\xc8\x69\x89\x01\xe8\x0c\xe3\xf6\x05\x48\x54\x58\x23\xdc\xc2\x91\x12\x5a\x43\xe1\xa4\x73\xb7\x6d\x77\xea\x5e\x3e\xc9\x61\x9d\xcf\xcb\x1b\x8a\x73\x6d\xd1\xa4\xd2\x64\x35\x71\x3c\xdd\x98\x86\xeb\x2c\x07\x79\xde\xbb\x32\xfa\xe8\x54\xc6\x23\x5f\xe5\xee\x71\x7a\x95\xf5\xf4\x46\x5f\x07\x65\x2b\xd6\xac\xfc\xf8\xbf\x32\xab\x60\xc8\x1f\x1d\xb9\x52\xcd\x13\x2f\xb2\xff\xf3\xcd\x62\x16\x3f\x01\x00\x00\xff\xff\xa0\x52\x35\x64\xfc\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\xcc\xc1\xca\xc2\x30\x10\x04\xe0\xfb\x3e\xc5\x90\xff\xbe\xb4\xe5\xc7\x5b\x7d\x0a\xef\x12\xdb\x55\x03\x69\x52\xb7\x49\x11\xa4\xef\x2e\x34\x44\xc4\x39\x7e\xcc\xcc\x1f\x4e\xa2\x6a\xaf\x51\x27\xac\xa2\x8b\x8b\x01\x36\x8c\x98\x7d\xbe\xb9\x50\x69\x21\x4a\x9f\xda\x8b\x00\x95\x47\x76\x2a\xe3\xb9\x6e\x7a\x98\x63\x8f\x86\xdb\x8e\x1b\x43\xdf\x8d\x59\xe3\xea\x46\xd1\x65\x1f\x02\x43\x42\x49\x0f\xd3\xf0\x81\x5b\xb3\x73\xc8\xde\x57\xee\xb8\xe5\xae\x70\x92\x69\xf6\x36\xc9\x0f\x4f\x36\x0d\xf7\x4b\x7c\x96\x93\xff\x72\xb2\xd1\x46\xef\x00\x00\x00\xff\xff\x2b\x2f\xc7\x9b\xd0\x00\x00\x00"),
 		},
 		"/terraform-modules/bootkube": &vfsgen۰DirInfo{
 			name:    "bootkube",
@@ -5609,9 +5666,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/bootkube/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 349,
+			uncompressedSize: 205,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x8e\x41\x0e\x82\x30\x10\x45\xf7\x3d\xc5\x0f\xee\x2b\xa0\x5b\x3c\x85\x7b\xd3\xc0\x28\x4d\x0a\x53\x67\x80\x8d\xe1\xee\x46\x04\x83\x09\x09\x9b\x26\xfd\xf3\xfe\x9b\x39\xe0\x4a\x22\xee\xce\xd2\x60\x20\x51\xcf\x2d\x5c\x5b\x21\x86\xfe\xe1\xdb\x25\x52\x63\xba\x1f\xf6\x32\x80\xd0\xb3\xf7\x42\xd5\x6d\xe9\x14\x48\x2e\x05\x52\x9b\x9d\x92\xf5\x38\x0a\x0f\xbe\x22\xd1\xa9\x05\x04\x2e\x5d\x40\x31\xff\x00\xe5\x5e\x4a\xc2\xa7\x5e\x3b\xad\x7d\xc9\x12\x8f\x13\x94\xcc\xc4\x6a\x41\x66\xcf\x36\xfd\xe6\xe3\xf4\x76\xd4\xc4\xe0\x3a\xda\x13\x2e\xdc\x86\x33\xb7\x99\xcd\xff\x9c\x41\x77\x75\x41\x37\x4d\xf9\xfa\xba\xd1\x8c\xe6\x1d\x00\x00\xff\xff\xac\x1b\x1a\x52\x5d\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\xcb\x3b\xae\xc2\x30\x10\x85\xe1\xde\xab\x38\xf2\xed\x47\x89\x2f\x6d\x58\x05\x3d\xb2\xf0\x80\x2c\xf9\xc5\xd8\x49\x83\xb2\x77\x94\x10\x23\xba\xd1\x37\xff\xf9\xc3\x85\x45\xec\x3d\x4b\xc4\xc2\x52\x7d\x4e\xb0\xc9\xa1\x84\xf9\xe1\x53\xa7\xaa\x54\xfb\x66\x2f\x05\x08\x3f\x67\x2f\xec\xae\x7d\x33\x41\x9f\x27\x0c\x34\x1a\xfd\xfb\x2e\x92\x17\xef\x58\xea\xbe\x02\x42\xbe\xd9\xb0\x1d\x13\xf4\x48\x27\x1a\xf4\xce\x8d\x63\x09\xb6\xf1\xc6\x86\x46\x32\x07\x87\x0a\x1c\xb5\x21\xd3\x6b\xb1\xc9\xe5\xd8\xf9\xff\xc3\xab\x5a\xd5\x3b\x00\x00\xff\xff\x0e\x1a\xe8\xad\xcd\x00\x00\x00"),
 		},
 		"/terraform-modules/dns": &vfsgen۰DirInfo{
 			name:    "dns",
@@ -5635,12 +5692,10 @@ var vfsgenAssets = func() http.FileSystem {
 
 			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x92\xbd\x8e\x14\x31\x10\x84\xf3\x79\x8a\xd2\x5c\xb2\x97\x4c\x74\xe9\x3d\xc0\x65\x17\x40\x42\xb2\xf2\xda\x35\x3b\x2d\x79\xdc\xa6\xdb\x5e\x38\x10\xef\x8e\x66\xff\x04\x02\x21\x81\x70\xec\xea\xef\x73\x97\x1f\xf0\x6e\x11\xc7\x2c\x99\x88\x5a\x5a\x90\xe2\x38\x05\x93\x70\xc8\x74\x7c\x5a\x24\x2e\x08\x46\xf8\x12\x8c\x09\x61\xd5\x72\x44\xc8\x19\xab\xa6\xbe\x5d\x91\x82\xb6\x8d\x48\x62\x8c\x4d\xed\x6d\xc2\x4b\x73\xd4\x6e\x55\x9d\x10\x1f\x1e\xd0\x14\xc6\xd4\x23\x91\x7a\xcd\x12\x43\x13\x2d\x08\x25\x21\xb8\x8b\xb7\x6d\x08\xcb\xac\x16\x65\x9b\x8e\xa8\xeb\xaa\x05\xa3\x94\x46\x9b\x43\xe4\x88\x59\xed\x8c\x6d\x0b\x6f\xe8\x69\x18\x6e\xa6\x18\x63\xee\xde\x68\xfb\x12\x56\x8e\xf8\x3a\x00\xed\xad\x12\xd7\xf3\x0c\x6f\x26\xe5\x38\x00\x89\x1e\x4d\xea\xd9\xe0\x19\xe3\xfb\x22\x1f\x3b\x71\x8d\x63\x8b\x63\x57\x8d\x95\x25\x31\x6d\xe6\xa9\xf8\xfe\x8b\x16\x3e\x8e\xc3\xb7\x9f\x88\x5a\x9a\x69\xce\x34\xdf\xd7\x7e\xc8\x12\xf7\x52\x4f\x4f\xbf\x83\x67\xf1\xb6\xbb\x18\x3c\xfe\xaa\xf0\x7a\x0e\xe3\xe5\xf5\xf4\x84\x90\x92\xd1\x9d\x0e\x9d\xef\xef\xfd\x81\x74\x59\xf7\xdd\xf7\x4f\x4a\x26\xa7\xd0\xf8\xaf\x4e\x97\xf4\xff\x90\xba\xed\xef\x6f\x5a\xf9\xa0\x85\x97\x2e\x7a\x49\xb4\xeb\x3f\x34\x46\xb5\xe4\xf0\x45\x7b\x4e\x38\x10\xd1\x18\x1a\x13\x76\x9c\x8e\x13\xf8\x39\xac\x35\x73\x8a\xba\x9e\xcb\xfa\x1e\x00\x00\xff\xff\x4e\xb2\x14\xd2\xdd\x02\x00\x00"),
 		},
-		"/terraform-modules/dns/cloudflare/versions.tf": &vfsgen۰CompressedFileInfo{
-			name:             "versions.tf",
-			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 162,
-
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x2a\x49\x2d\x2a\x4a\x4c\xcb\x2f\xca\x55\xa8\xe6\x52\x50\x28\x4a\x2d\x2c\xcd\x2c\x4a\x4d\x89\x2f\x4b\x2d\x2a\xce\xcc\xcf\x53\xb0\x55\x50\xb2\xb3\x55\x30\xd0\x33\x34\x56\xe2\x42\x96\x2f\x28\xca\x2f\xcb\x4c\x49\x2d\x2a\x06\x6b\x53\x50\x48\xce\xc9\x2f\x4d\x49\xcb\x49\x2c\x4a\x55\xb0\x85\x0a\x29\x28\x14\xe7\x97\x16\x25\xa7\x2a\x80\x0c\x41\xc8\xeb\x23\x98\x4a\x50\x75\x48\x96\x19\xe9\x59\xea\x19\x40\xc4\x6b\xb9\x40\xb8\x96\x0b\x10\x00\x00\xff\xff\x19\xf3\x50\x28\xa2\x00\x00\x00"),
+		"/terraform-modules/dns/cloudflare/versions.tf": &vfsgen۰FileInfo{
+			name:    "versions.tf",
+			modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			content: []byte("\x74\x65\x72\x72\x61\x66\x6f\x72\x6d\x20\x7b\x0a\x20\x20\x72\x65\x71\x75\x69\x72\x65\x64\x5f\x76\x65\x72\x73\x69\x6f\x6e\x20\x3d\x20\x22\x3e\x3d\x20\x30\x2e\x31\x32\x2e\x30\x22\x0a\x0a\x20\x20\x72\x65\x71\x75\x69\x72\x65\x64\x5f\x70\x72\x6f\x76\x69\x64\x65\x72\x73\x20\x7b\x0a\x20\x20\x20\x20\x63\x6c\x6f\x75\x64\x66\x6c\x61\x72\x65\x20\x3d\x20\x22\x32\x2e\x39\x2e\x30\x22\x0a\x20\x20\x7d\x0a\x7d\x0a"),
 		},
 		"/terraform-modules/dns/manual": &vfsgen۰DirInfo{
 			name:    "manual",
@@ -5683,12 +5738,10 @@ var vfsgenAssets = func() http.FileSystem {
 
 			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\xac\x92\xbd\x8e\x14\x31\x10\x84\xf3\x79\x8a\xd2\x5c\xb2\x97\x4c\x74\xe9\x3d\xc0\x65\x17\x40\x42\xb2\xf2\xda\x35\x3b\x2d\x79\xdc\xa6\xdb\x5e\x38\x10\xef\x8e\x66\xff\x04\x02\x21\x81\x70\xec\xea\xef\x73\x97\x1f\xf0\x6e\x11\xc7\x2c\x99\x88\x5a\x5a\x90\xe2\x38\x05\x93\x70\xc8\x74\x7c\x5a\x24\x2e\x08\x46\xf8\x12\x8c\x09\x61\xd5\x72\x44\xc8\x19\xab\xa6\xbe\x5d\x91\x82\xb6\x8d\x48\x62\x8c\x4d\xed\x6d\xc2\x4b\x73\xd4\x6e\x55\x9d\x10\x1f\x1e\xd0\x14\xc6\xd4\x23\x91\x7a\xcd\x12\x43\x13\x2d\x08\x25\x21\xb8\x8b\xb7\x6d\x08\xcb\xac\x16\x65\x9b\x8e\xa8\xeb\xaa\x05\xa3\x94\x46\x9b\x43\xe4\x88\x59\xed\x8c\x6d\x0b\x6f\xe8\x69\x18\x6e\xa6\x18\x63\xee\xde\x68\xfb\x12\x56\x8e\xf8\x3a\x00\xed\xad\x12\xd7\xf3\x0c\x6f\x26\xe5\x38\x00\x89\x1e\x4d\xea\xd9\xe0\x19\xe3\xfb\x22\x1f\x3b\x71\x8d\x63\x8b\x63\x57\x8d\x95\x25\x31\x6d\xe6\xa9\xf8\xfe\x8b\x16\x3e\x8e\xc3\xb7\x9f\x88\x5a\x9a\x69\xce\x34\xdf\xd7\x7e\xc8\x12\xf7\x52\x4f\x4f\xbf\x83\x67\xf1\xb6\xbb\x18\x3c\xfe\xaa\xf0\x7a\x0e\xe3\xe5\xf5\xf4\x84\x90\x92\xd1\x9d\x0e\x9d\xef\xef\xfd\x81\x74\x59\xf7\xdd\xf7\x4f\x4a\x26\xa7\xd0\xf8\xaf\x4e\x97\xf4\xff\x90\xba\xed\xef\x6f\x5a\xf9\xa0\x85\x97\x2e\x7a\x49\xb4\xeb\x3f\x34\x46\xb5\xe4\xf0\x45\x7b\x4e\x38\x10\xd1\x18\x1a\x13\x76\x9c\x8e\x13\xf8\x39\xac\x35\x73\x8a\xba\x9e\xcb\xfa\x1e\x00\x00\xff\xff\x4e\xb2\x14\xd2\xdd\x02\x00\x00"),
 		},
-		"/terraform-modules/dns/route53/versions.tf": &vfsgen۰CompressedFileInfo{
-			name:             "versions.tf",
-			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 147,
-
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\x8c\x41\x0e\x82\x30\x14\x44\xf7\x3d\xc5\xa4\x07\xa8\x98\xae\xeb\x55\x4c\x03\xdf\xf0\x17\xfa\x71\xbe\xc0\xc2\x70\x77\x83\xb0\xe8\x62\x92\xc9\xbc\xbc\xf9\x08\x59\x1f\xc6\x27\xbe\x01\xa0\xbc\x67\xa5\x0c\xf7\x45\xe8\x6a\x2f\x14\xc4\x5b\x41\x97\xae\x39\x86\x96\x4f\xb4\x45\x07\xa1\xff\x35\xa0\xae\x8e\x72\x76\xc0\x6d\x66\x2f\xd8\xed\xb1\xfa\xa8\xbd\x71\xba\xd4\xd5\xe3\xc9\x9b\xf7\x9c\x72\xea\x8e\x7d\x0b\x7b\xb6\xf0\x0b\x00\x00\xff\xff\x7f\xf8\x2c\x24\x93\x00\x00\x00"),
+		"/terraform-modules/dns/route53/versions.tf": &vfsgen۰FileInfo{
+			name:    "versions.tf",
+			modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
+			content: []byte("\x74\x65\x72\x72\x61\x66\x6f\x72\x6d\x20\x7b\x0a\x20\x20\x72\x65\x71\x75\x69\x72\x65\x64\x5f\x76\x65\x72\x73\x69\x6f\x6e\x20\x3d\x20\x22\x3e\x3d\x20\x30\x2e\x31\x32\x2e\x30\x22\x0a\x0a\x20\x20\x72\x65\x71\x75\x69\x72\x65\x64\x5f\x70\x72\x6f\x76\x69\x64\x65\x72\x73\x20\x7b\x0a\x20\x20\x20\x20\x61\x77\x73\x20\x3d\x20\x22\x33\x2e\x33\x2e\x30\x22\x0a\x20\x20\x7d\x0a\x7d\x0a"),
 		},
 		"/terraform-modules/dns/shared-variables.tf": &vfsgen۰CompressedFileInfo{
 			name:             "shared-variables.tf",
@@ -5786,9 +5839,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/google-cloud/flatcar-linux/kubernetes/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 401,
+			uncompressedSize: 186,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x8e\xcb\x6a\xc6\x20\x10\x85\xf7\x3e\xc5\x60\xf7\xe6\x52\xe8\xce\x3e\x45\xf7\x45\x74\x9a\x08\xc6\xb1\xa3\x66\x53\xf2\xee\x25\xb7\x36\xd0\x42\xfe\x8d\xe0\x9c\xef\x7c\x33\x4f\xf0\x86\xcc\xe6\x83\x78\x82\x19\x39\x7b\x8a\x60\xa2\x83\x14\xea\xe0\xe3\x39\xca\x42\x94\x1f\xec\x4b\x00\x30\x7e\x56\xcf\xe8\xde\xcf\x8e\x06\xf9\xaa\xa1\x55\xdd\xb3\x14\xd7\x3c\x31\xcd\xde\x21\xe7\xad\x06\x30\x10\x0d\x01\x41\x1f\x5f\x80\x4c\x95\x2d\xc2\x2a\x18\x4d\x1e\xbd\x25\x4e\xcd\x4e\xc9\x03\xb9\xec\xe8\x55\xf7\xa2\xda\x3d\x58\xb6\x37\xd6\x10\xee\x74\x2b\xf3\xbf\x4c\xf5\x57\x57\xc1\x29\x05\x53\x6e\xcf\x3b\xb9\x47\x9c\xb6\xfc\xb5\x69\x90\x89\x32\x7a\x47\xb1\xb1\xe5\x97\x5e\xc4\x22\xbe\x03\x00\x00\xff\xff\x96\x73\xb5\xe9\x91\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x54\xcb\xb1\x0e\xc2\x20\x14\x85\xe1\xfd\x3e\xc5\x49\xdd\x49\xcb\xe0\x56\x9f\xc2\xdd\x90\x70\x25\x24\x14\xf0\x02\x5d\x4c\xdf\xdd\xa0\xd6\xd8\xe9\x24\x5f\xce\x7f\xc2\x95\x45\xcc\x3d\xc9\x82\x95\xa5\xf8\x14\x61\xa2\x45\x0e\xcd\xf9\xb8\x53\x21\xaa\xbf\xdb\x93\x00\xe1\x47\xf3\xc2\xf6\xb6\x37\x33\x86\xcb\x8c\x51\x4d\x5a\x8d\x03\xfd\x3f\xb2\xa4\xd5\x5b\x96\xf2\x0e\x01\x97\x92\x0b\x0c\xf4\x44\xab\xe9\xdc\xff\xdd\x63\x0b\xa1\xef\xd7\x95\xfe\x70\xe5\x25\x07\x53\xf9\xc0\x1b\x6d\xf4\x0a\x00\x00\xff\xff\x15\x5b\x2c\x17\xba\x00\x00\x00"),
 		},
 		"/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers": &vfsgen۰DirInfo{
 			name:    "workers",
@@ -5829,9 +5882,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 284,
+			uncompressedSize: 122,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x84\x8d\x4d\x0a\xc2\x30\x10\x46\xf7\x39\xc5\x47\x0e\x90\xfe\x08\xee\xe2\x55\xa4\xa4\x63\x1b\x68\x3b\x71\x92\x76\x23\xbd\xbb\xd8\x56\x0c\x28\xb8\x19\x18\xde\x9b\x37\x89\x44\x9a\x1b\xcb\x88\x87\x02\x84\xee\xb3\x17\x6a\xaf\x0b\x49\xf4\x3c\xc1\x42\x5f\x2c\x4a\x53\x9d\xb4\xca\x79\x10\x5e\x7c\x4b\x12\xb7\x33\xa0\x63\xee\x06\x82\x3d\x56\x20\xf2\x2c\x8e\xf0\x0a\xf4\x4d\xec\xbd\x63\x09\xc5\x6e\xe9\x43\xc9\x7e\xd4\xa6\x3a\x9b\x72\x07\xeb\x36\x13\x8d\x61\x68\xd2\xdf\xe4\xdb\xfb\x1d\x35\x75\xde\x74\xe9\xbb\x66\xa1\x03\x47\xf2\x2d\x4f\x85\x4b\x1f\x7b\x55\xab\x7a\x06\x00\x00\xff\xff\x39\x69\x74\x19\x1c\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\xcb\x31\x0e\x82\x40\x10\x85\xe1\x7e\x4f\xf1\xc2\x01\x26\xb0\x85\x1d\x5e\xc5\x90\xf0\x24\x9b\x80\x83\x8f\x95\xc6\xec\xdd\xcd\x18\x0b\xdb\x2f\xff\x5f\x29\x4d\x77\xd7\x86\x77\x02\xc4\xe7\xab\x88\xf3\xed\xa4\x8e\xe2\x0f\x8c\xe8\xae\x23\x7a\x1b\xb2\xf5\x5d\xfa\x2f\x76\xf9\x59\x66\xea\xf8\x8e\xc0\xe2\xbe\xac\x04\x62\xc9\x36\x5c\xa2\x0f\xaf\xdc\xf6\x75\xaa\xfc\xb9\xe5\xe0\x96\x5a\xfa\x04\x00\x00\xff\xff\xdb\x99\x6b\xea\x7a\x00\x00\x00"),
 		},
 		"/terraform-modules/google-cloud/flatcar-linux/kubernetes/workers/workers.tf": &vfsgen۰CompressedFileInfo{
 			name:             "workers.tf",
@@ -6037,9 +6090,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/packet/flatcar-linux/kubernetes/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 584,
+			uncompressedSize: 231,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x8c\x90\xc1\x4e\xc4\x30\x0c\x44\xef\xf9\x0a\xab\xdc\xdd\x76\x8b\xb8\x95\xaf\xe0\x8e\xa2\xc4\xd0\x88\x34\x0e\x4e\xba\x17\xd4\x7f\x47\xdb\x6e\x51\x90\x82\xca\xc5\xb2\x3c\x33\x4f\xd6\x3c\xc0\x0b\x89\xe8\x37\x96\x19\xae\x24\xc9\x71\x00\x1d\x2c\x44\xbf\xbc\xbb\x70\x9c\x92\x52\xf9\xc7\xf6\xa5\x00\x84\x3e\x17\x27\x64\x5f\x8f\xcc\x08\xcd\xf3\x08\x1d\xf6\x43\xa3\x4a\x3d\x0a\x5f\x9d\x25\x49\x5b\x0c\xc0\x64\x18\xef\x2b\x40\xe2\x45\x0c\xc1\x2d\x1c\x39\x91\xb3\x1c\x5a\x93\x9b\xbb\x5a\xa0\x3b\x7c\xc2\x7e\xbf\xaf\xdb\xf4\x6c\xb4\xaf\x93\x26\x9d\x26\x67\x58\x62\xbb\x99\x2a\xb4\x1e\x1f\xb1\x2b\x69\x61\xf1\xa7\xb0\x9b\xa7\xc2\xba\x60\x8f\x97\x92\x95\x69\x8e\x5e\x67\x3a\xe3\x1d\xbe\xff\x30\xa3\x36\x1f\xf4\x57\x71\x9b\x36\x71\xca\xed\xbe\x56\x80\x03\x76\xbf\xeb\x13\x1d\x2c\xcf\x67\x2f\xee\xae\xea\x83\x43\x59\xe0\xaa\x56\xf5\x1d\x00\x00\xff\xff\x1f\x31\xe2\x62\x48\x02\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x5c\x8c\xc1\xae\x82\x30\x10\x45\xf7\xfd\x8a\x9b\xbe\xfd\xa4\xe5\x19\x77\xf8\x15\xee\x4d\x03\xa3\x69\x2c\x6d\x1d\x0a\x1b\xc3\xbf\x1b\xa8\x18\xe3\xec\xe6\xe4\xdc\xf3\x87\x33\x8b\xb8\x6b\x92\x01\x33\xcb\xe8\x53\x84\x8b\x3d\x72\x98\x6e\x3e\xee\x68\x54\xaa\x7c\xb4\xa7\x02\x84\x1f\x93\x17\xee\x2f\xfb\xa6\x85\x3e\xb5\x30\x64\x1b\x32\x5a\x7d\x1b\x59\xd2\xec\x7b\x96\x71\x1b\x02\x5d\x41\xbd\x16\xda\xd0\x91\xac\xde\x70\x48\x9d\x0b\x6f\x6c\xe9\xb0\x56\xd6\x2f\x4e\x21\xec\x76\x43\x96\x9a\x8a\x0b\x0f\x39\xb8\xc2\x3f\x38\xbb\xee\xce\xa5\xda\xff\x64\x6a\x64\x51\x8b\x7a\x05\x00\x00\xff\xff\x53\x74\x30\x63\xe7\x00\x00\x00"),
 		},
 		"/terraform-modules/packet/flatcar-linux/kubernetes/workers": &vfsgen۰DirInfo{
 			name:    "workers",
@@ -6094,9 +6147,9 @@ var vfsgenAssets = func() http.FileSystem {
 		"/terraform-modules/packet/flatcar-linux/kubernetes/workers/versions.tf": &vfsgen۰CompressedFileInfo{
 			name:             "versions.tf",
 			modTime:          time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC),
-			uncompressedSize: 346,
+			uncompressedSize: 158,
 
-			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x74\xcc\x4d\x8e\xc2\x30\x0c\x05\xe0\x7d\x4e\x61\x75\xf6\x69\x3a\x95\x66\xd7\x39\x05\x7b\x14\x25\x86\x46\xd0\x38\xd8\x69\x37\xa8\x77\x47\xf4\x07\x8a\x54\x36\x96\xe5\xe7\xf7\xfd\xc0\x01\x99\xed\x89\xb8\x83\x01\x59\x02\x45\xb0\xd1\x43\xba\xf6\xe7\x10\xd7\x93\x28\x95\x5f\x6f\x77\x05\xc0\x78\xeb\x03\xa3\x3f\xae\x9d\x06\x8a\xff\x06\x8c\xae\xea\x42\x6d\xf3\xc4\x34\x04\x8f\x2c\x53\x0d\xc0\x65\x68\x96\x15\x40\xa8\x67\x87\xf0\x2c\x27\x12\x0c\x9e\x62\xe9\x72\xb1\xa4\x1b\xda\xe8\x3f\x5d\xcd\xf7\x71\x9a\xc9\xba\x0b\x7e\xa3\xa6\xac\x25\xc9\xe5\xbc\xee\x80\xb5\x36\x9f\x20\xdb\xe8\xa9\xdb\x07\x5b\x2b\x6d\x70\xc4\xa9\x9c\xbf\x76\xbc\x5f\x5d\x6b\xf3\xf6\x46\x35\xaa\x47\x00\x00\x00\xff\xff\xd3\xa9\xee\xe2\x5a\x01\x00\x00"),
+			compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x00\xff\x4c\x8c\xb1\x0a\xc2\x30\x14\x45\xf7\xf7\x15\x97\xb8\x3f\x52\x05\xb7\xfa\x15\xee\x12\x9a\xa7\x04\x35\x89\x2f\x69\x17\xe9\xbf\x4b\x0a\x91\x2e\x77\x38\x9c\x73\x0f\xb8\x8a\xaa\xbb\x27\x7d\x63\x11\x2d\x21\x45\xb8\xe8\x91\x5f\xf3\x23\xc4\x8e\x0a\x51\xfd\x6b\x5f\x02\x54\x3e\x73\x50\xf1\xb7\xde\x8c\x30\x97\x11\x96\x87\x23\x5b\x43\x7b\x23\x6b\x5a\x82\x17\x2d\x5b\x08\x4c\xb5\x6d\x0b\x2c\x9f\x79\x30\x1b\xcc\x6e\x7a\x4a\x6d\xf0\xc4\xb6\x3d\x00\x2b\xad\xf4\x0b\x00\x00\xff\xff\x80\x78\x50\x65\x9e\x00\x00\x00"),
 		},
 		"/terraform-modules/packet/flatcar-linux/kubernetes/workers/workers.tf": &vfsgen۰CompressedFileInfo{
 			name:             "workers.tf",
@@ -6120,6 +6173,7 @@ var vfsgenAssets = func() http.FileSystem {
 		fs["/charts/components/external-dns"].(os.FileInfo),
 		fs["/charts/components/flatcar-linux-update-operator"].(os.FileInfo),
 		fs["/charts/components/gangway"].(os.FileInfo),
+		fs["/charts/components/headlamp"].(os.FileInfo),
 		fs["/charts/components/httpbin"].(os.FileInfo),
 		fs["/charts/components/istio-operator"].(os.FileInfo),
 		fs["/charts/components/linkerd2"].(os.FileInfo),
@@ -6318,6 +6372,18 @@ var vfsgenAssets = func() http.FileSystem {
 		fs["/charts/components/gangway/templates/secret.yaml"].(os.FileInfo),
 		fs["/charts/components/gangway/templates/service.yaml"].(os.FileInfo),
 	}
+	fs["/charts/components/headlamp"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
+		fs["/charts/components/headlamp/.helmignore"].(os.FileInfo),
+		fs["/charts/components/headlamp/Chart.yaml"].(os.FileInfo),
+		fs["/charts/components/headlamp/templates"].(os.FileInfo),
+		fs["/charts/components/headlamp/values.yaml"].(os.FileInfo),
+	}
+	fs["/charts/components/headlamp/templates"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
+		fs["/charts/components/headlamp/templates/_helpers.tpl"].(os.FileInfo),
+		fs["/charts/components/headlamp/templates/deployment.yaml"].(os.FileInfo),
+		fs["/charts/components/headlamp/templates/ingress.yaml"].(os.FileInfo),
+		fs["/charts/components/headlamp/templates/service.yaml"].(os.FileInfo),
+	}
 	fs["/charts/components/httpbin"].(*vfsgen۰DirInfo).entries = []os.FileInfo{
 		fs["/charts/components/httpbin/.helmignore"].(os.FileInfo),
 		fs["/charts/components/httpbin/Chart.yaml"].(os.FileInfo),
diff --git a/pkg/assets/walkers.go b/pkg/assets/walkers.go
index f3735d36..09e23d96 100644
--- a/pkg/assets/walkers.go
+++ b/pkg/assets/walkers.go
@@ -17,12 +17,13 @@ package assets
 import (
 	"fmt"
 	"io"
+	"io/ioutil"
 	"os"
 	"path/filepath"
 )
 
-// copyingWalker returns a WalkFunc which writes the given file to disk.
-func copyingWalker(path string, newDirPerms os.FileMode) walkFunc {
+// CopyingWalker returns a WalkFunc which writes the given file to disk.
+func CopyingWalker(path string, newDirPerms os.FileMode) WalkFunc {
 	return func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
 		if err != nil {
 			return fmt.Errorf("error while walking at %q: %w", fileName, err)
@@ -38,6 +39,39 @@ func copyingWalker(path string, newDirPerms os.FileMode) walkFunc {
 	}
 }
 
+// DumpingWalker returns a WalkFunc which sets the contents of the given file in a map.
+func DumpingWalker(contentsMap map[string]string, allowedExts ...string) WalkFunc {
+	var extsMap map[string]struct{}
+
+	if len(allowedExts) > 0 {
+		extsMap = make(map[string]struct{}, len(allowedExts))
+		for _, ext := range allowedExts {
+			extsMap[ext] = struct{}{}
+		}
+	}
+
+	return func(fileName string, fileInfo os.FileInfo, r io.ReadSeeker, err error) error {
+		if err != nil {
+			return fmt.Errorf("error while walking at %q: %w", fileName, err)
+		}
+
+		if extsMap != nil {
+			if _, ok := extsMap[filepath.Ext(fileName)]; !ok {
+				return nil
+			}
+		}
+
+		contents, err := ioutil.ReadAll(r)
+		if err != nil {
+			return fmt.Errorf("failed to read %q: %w", fileName, err)
+		}
+
+		contentsMap[fileName] = string(contents)
+
+		return nil
+	}
+}
+
 // writeFile writes data from given io.Reader to the file and makes sure, that
 // this is the only content stored in the file.
 func writeFile(p string, r io.Reader) error {
diff --git a/pkg/components/headlamp/component.go b/pkg/components/headlamp/component.go
new file mode 100644
index 00000000..9efc3429
--- /dev/null
+++ b/pkg/components/headlamp/component.go
@@ -0,0 +1,94 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+package headlamp
+
+import (
+	"fmt"
+
+	"github.com/hashicorp/hcl/v2"
+	"github.com/hashicorp/hcl/v2/gohcl"
+
+	"github.com/kinvolk/lokomotive/internal/template"
+	"github.com/kinvolk/lokomotive/pkg/components"
+	"github.com/kinvolk/lokomotive/pkg/components/types"
+	"github.com/kinvolk/lokomotive/pkg/components/util"
+	"github.com/kinvolk/lokomotive/pkg/k8sutil"
+)
+
+const name = "headlamp"
+
+//nolint:gochecknoinits
+func init() {
+	components.Register(name, newComponent())
+}
+
+type component struct {
+	Namespace string         `hcl:"namespace,optional"`
+	Ingress   *types.Ingress `hcl:"ingress,block"`
+}
+
+func newComponent() *component {
+	return &component{
+		Namespace: "lokomotive-system",
+	}
+}
+
+// LoadConfig loads the component config.
+func (c *component) LoadConfig(configBody *hcl.Body, evalContext *hcl.EvalContext) hcl.Diagnostics {
+	if configBody == nil {
+		return hcl.Diagnostics{
+			components.HCLDiagConfigBodyNil,
+		}
+	}
+
+	if err := gohcl.DecodeBody(*configBody, evalContext, c); err != nil {
+		return err
+	}
+
+	if c.Ingress != nil {
+		c.Ingress.SetDefaults()
+	}
+
+	return nil
+}
+
+// RenderManifests renders the Helm chart templates with values provided.
+func (c *component) RenderManifests() (map[string]string, error) {
+	helmChart, err := components.Chart(name)
+	if err != nil {
+		return nil, fmt.Errorf("retrieving chart from assets: %w", err)
+	}
+
+	values, err := template.Render(chartValuesTmpl, c)
+	if err != nil {
+		return nil, fmt.Errorf("rendering chart values template: %w", err)
+	}
+
+	renderedFiles, err := util.RenderChart(helmChart, name, c.Metadata().Namespace.Name, values)
+	if err != nil {
+		return nil, fmt.Errorf("rendering chart: %w", err)
+	}
+
+	return renderedFiles, nil
+}
+
+func (c *component) Metadata() components.Metadata {
+	return components.Metadata{
+		Name: name,
+		Namespace: k8sutil.Namespace{
+			Name: c.Namespace,
+		},
+	}
+}
diff --git a/pkg/components/headlamp/doc.go b/pkg/components/headlamp/doc.go
new file mode 100644
index 00000000..7fbd2d8f
--- /dev/null
+++ b/pkg/components/headlamp/doc.go
@@ -0,0 +1,16 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+// Package headlamp contains code for the Headlamp component.
+package headlamp
diff --git a/pkg/components/headlamp/template.go b/pkg/components/headlamp/template.go
new file mode 100644
index 00000000..4bc4c241
--- /dev/null
+++ b/pkg/components/headlamp/template.go
@@ -0,0 +1,46 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+package headlamp
+
+const chartValuesTmpl = `
+nodeSelector:
+  beta.kubernetes.io/os: linux
+image:
+  # TODO: Use v0.1.0 when it's released.
+  tag: l8e
+  pullPolicy: Always
+securityContext:
+  capabilities:
+    drop:
+    - ALL
+  runAsNonRoot: true
+  runAsUser: 1000
+{{- if .Ingress }}
+ingress:
+  enabled: true
+  hosts:
+  - host: {{ .Ingress.Host }}
+    paths:
+    - /
+  tls:
+  - secretName: {{ .Ingress.Host }}-tls
+    hosts:
+    - {{ .Ingress.Host }}
+  annotations:
+    kubernetes.io/ingress.class: {{ .Ingress.Class }}
+    cert-manager.io/cluster-issuer: {{ .Ingress.CertManagerClusterIssuer }}
+    contour.heptio.com/websocket-routes: "/"
+{{- end }}
+`
diff --git a/pkg/helm/charts.go b/pkg/helm/charts.go
index 9ebc289e..7f7951ff 100644
--- a/pkg/helm/charts.go
+++ b/pkg/helm/charts.go
@@ -46,7 +46,9 @@ func ChartFromAssets(location string) (*chart.Chart, error) {
 	// isn't trivial since we call os.RemoveAll() in a defer statement.
 	defer os.RemoveAll(tmpDir) //nolint: errcheck
 
-	if err := assets.Extract(location, tmpDir); err != nil {
+	// Rendered files could contain secrets - allow r/w access to owner only.
+	walk := assets.CopyingWalker(tmpDir, 0700)
+	if err := assets.Assets.WalkFiles(location, walk); err != nil {
 		return nil, fmt.Errorf("traversing assets: %w", err)
 	}
 
diff --git a/pkg/platform/aks/aks.go b/pkg/platform/aks/aks.go
index 27f6e4a9..754104ed 100644
--- a/pkg/platform/aks/aks.go
+++ b/pkg/platform/aks/aks.go
@@ -82,7 +82,7 @@ const (
 	subscriptionIDEnv = "LOKOMOTIVE_AKS_SUBSCRIPTION_ID"
 	tenantIDEnv       = "LOKOMOTIVE_AKS_TENANT_ID"
 
-	kubernetesVersion = "1.18.8"
+	kubernetesVersion = "1.17.9"
 )
 
 // init registers AKS as a platform.
@@ -334,7 +334,8 @@ func (c *config) PostApplyHook(kubeconfig []byte) error {
 // waitForDefaultStorageClass waits until the default storage class appears on a given cluster.
 // If it doesn't appear within a defined time range, an error is returned.
 func waitForDefaultStorageClass(sci v1.StorageClassInterface) error {
-	defaultStorageClassAnnotation := "storageclass.kubernetes.io/is-default-class"
+	// AKS still uses annotation with .beta.
+	defaultStorageClassAnnotation := "storageclass.beta.kubernetes.io/is-default-class"
 
 	if err := wait.PollImmediate(retryInterval, timeout, func() (done bool, err error) {
 		scs, err := sci.List(context.TODO(), metav1.ListOptions{})
diff --git a/pkg/platform/aks/template.go b/pkg/platform/aks/template.go
index ff4fc47f..70424ad7 100644
--- a/pkg/platform/aks/template.go
+++ b/pkg/platform/aks/template.go
@@ -54,34 +54,25 @@ locals {
 }
 
 provider "azurerm" {
+  version = "2.24.0"
+
   # https://github.com/terraform-providers/terraform-provider-azurerm/issues/5893
   features {}
 }
 
-terraform {
-  required_providers {
-    azurerm = {
-      source  = "hashicorp/azurerm"
-      version = "2.31.1"
-    }
-    local = {
-      source  = "hashicorp/local"
-      version = "1.4.0"
-    }
-{{- if .ApplicationName }}
-    azuread = {
-      source  = "hashicorp/azuread"
-      version = "1.0.0"
-    }
-    random = {
-      source  = "hashicorp/random"
-      version = "3.0.0"
-    }
-{{- end }}
-  }
+provider "local" {
+  version = "1.4.0"
 }
 
 {{- if .ApplicationName }}
+provider "azuread" {
+  version = "0.11.0"
+}
+
+provider "random" {
+  version = "2.3.0"
+}
+
 resource "azuread_application" "aks" {
   name = local.application_name
 }
diff --git a/pkg/platform/aws/template.go b/pkg/platform/aws/template.go
index fc47fe39..988ead6f 100644
--- a/pkg/platform/aws/template.go
+++ b/pkg/platform/aws/template.go
@@ -201,12 +201,34 @@ module "worker-pool-{{ $index }}" {
 {{- end }}
 
 provider "aws" {
+  version = "3.3.0"
+
   region                  = "{{.Config.Region}}"
   {{- if .Config.CredsPath }}
   shared_credentials_file = "{{.Config.CredsPath}}"
   {{- end }}
 }
 
+provider "ct" {
+  version = "0.6.1"
+}
+
+provider "local" {
+  version = "1.4.0"
+}
+
+provider "null" {
+  version = "2.1.2"
+}
+
+provider "template" {
+  version = "2.1.2"
+}
+
+provider "tls" {
+  version = "2.2.0"
+}
+
 # Stub output, which indicates, that Terraform run at least once.
 # Used when checking, if we should ask user for confirmation, when
 # applying changes to the cluster.
diff --git a/pkg/platform/baremetal/template.go b/pkg/platform/baremetal/template.go
index b1000f51..4f0fcea3 100644
--- a/pkg/platform/baremetal/template.go
+++ b/pkg/platform/baremetal/template.go
@@ -69,22 +69,34 @@ module "bare-metal-{{.ClusterName}}" {
   {{- end}}
 }
 
-terraform {
-  required_providers {
-    matchbox = {
-      source  = "poseidon/matchbox"
-      version = "0.4.1"
-    }
-  }
-}
-
 provider "matchbox" {
+  version     = "0.4.1"
   endpoint    = "{{.MatchboxEndpoint}}"
   client_cert = file("{{.MatchboxClientCert}}")
   client_key  = file("{{.MatchboxClientKey}}")
   ca          = file("{{.MatchboxCA}}")
 }
 
+provider "ct" {
+  version = "0.6.1"
+}
+
+provider "local" {
+  version = "1.4.0"
+}
+
+provider "null" {
+  version = "2.1.2"
+}
+
+provider "template" {
+  version = "2.1.2"
+}
+
+provider "tls" {
+  version = "2.2.0"
+}
+
 # Stub output, which indicates, that Terraform run at least once.
 # Used when checking, if we should ask user for confirmation, when
 # applying changes to the cluster.
diff --git a/pkg/platform/packet/template.go b/pkg/platform/packet/template.go
index 7605f793..52bb1a19 100644
--- a/pkg/platform/packet/template.go
+++ b/pkg/platform/packet/template.go
@@ -260,6 +260,7 @@ output "dns_entries" {
 
 {{- if eq .Config.DNS.Provider "route53" }}
 provider "aws" {
+  version = "3.3.0"
   # The Route 53 service doesn't need a specific region to operate, however
   # the AWS Terraform provider needs it and the documentation suggests to use
   # "us-east-1": https://docs.aws.amazon.com/general/latest/gr/r53.html.
@@ -267,16 +268,29 @@ provider "aws" {
 }
 {{- end }}
 
-terraform {
-  required_providers {
-    packet = {
-      source  = "packethost/packet"
-      version = "3.0.1"
-    }
-  }
+provider "ct" {
+  version = "0.6.1"
+}
+
+provider "local" {
+  version = "1.4.0"
+}
+
+provider "null" {
+  version = "2.1.2"
+}
+
+provider "template" {
+  version = "2.1.2"
+}
+
+provider "tls" {
+  version = "2.2.0"
 }
 
 provider "packet" {
+  version = "3.0.0"
+
   {{- if .Config.AuthToken }}
   auth_token = "{{.Config.AuthToken}}"
   {{- end }}
diff --git a/pkg/terraform/executor.go b/pkg/terraform/executor.go
index 098e3b6f..5adc781b 100644
--- a/pkg/terraform/executor.go
+++ b/pkg/terraform/executor.go
@@ -43,7 +43,7 @@ const (
 	logsFileSuffix = ".log"
 	failFileSuffix = ".fail"
 
-	requiredVersion = ">= 0.13, < 0.14"
+	requiredVersion = ">= 0.12, < 0.13"
 
 	noOfLinesOnError = 20
 )
@@ -391,11 +391,7 @@ func (ex *Executor) executeSync(args ...string) ([]byte, error) {
 
 	h.stop()
 
-	if err != nil {
-		return nil, fmt.Errorf("executing with arguments '%s': %w", strings.Join(args, ", "), err)
-	}
-
-	return output, nil
+	return output, err
 }
 
 // Plan runs 'terraform plan'.
@@ -518,22 +514,25 @@ func (ex *Executor) logPath(id int) string {
 }
 
 func (ex *Executor) checkVersion() error {
-	versionOutputRaw, err := ex.executeSync("version", "-json")
+	vOutput, err := ex.executeSync("--version")
 	if err != nil {
-		return fmt.Errorf("executing 'terraform version -json': %w", err)
+		return fmt.Errorf("executing 'terraform --version': %w", err)
 	}
 
-	versionOutput := struct {
-		TerraformVersion string `json:"terraform_version"`
-	}{}
+	format := "Terraform v%s\n"
+	var vStr string
+	n, err := fmt.Sscanf(string(vOutput), format, &vStr)
+	if err != nil {
+		return fmt.Errorf("output %q does not match format %q: %w", string(vOutput), format, err)
+	}
 
-	if err := json.Unmarshal(versionOutputRaw, &versionOutput); err != nil {
-		return fmt.Errorf("unmarshaling version command output %q: %w", string(versionOutputRaw), err)
+	if n != 1 {
+		return fmt.Errorf("version not found in 'terraform --version' output")
 	}
 
-	v, err := version.NewVersion(versionOutput.TerraformVersion)
+	v, err := version.NewVersion(vStr)
 	if err != nil {
-		return fmt.Errorf("parsing Terraform version %q: %w", versionOutput.TerraformVersion, err)
+		return fmt.Errorf("parsing Terraform version %q: %w", vStr, err)
 	}
 
 	// requiredVersion is const, so we test it in unit tests.
diff --git a/scripts/find-updates.sh b/scripts/find-updates.sh
index cdbe122c..ae54ad6d 100755
--- a/scripts/find-updates.sh
+++ b/scripts/find-updates.sh
@@ -43,7 +43,7 @@ printf "${format}" "calico" "${current_version}" "${version}"
 
 ###########################
 # etcd
-current_version=$(grep 'ETCD_IMAGE_TAG=' assets/terraform-modules/aws/flatcar-linux/kubernetes/cl/controller.yaml.tmpl | cut -d"=" -f2 | sed 's/"//g')
+current_version=$(grep 'ETCD_IMAGE_TAG=' assets/terraform-modules/aws/flatcar-linux/kubernetes/cl/controller.yaml.tmpl | cut -d"=" -f3 | sed 's/"//g')
 
 get_latest_release etcd-io/etcd
 printf "${format}" "etcd" "${current_version}" "${version}"


Test patch:
diff --git a/pkg/components/headlamp/component_test.go b/pkg/components/headlamp/component_test.go
new file mode 100644
index 00000000..64345571
--- /dev/null
+++ b/pkg/components/headlamp/component_test.go
@@ -0,0 +1,221 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+package headlamp_test
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/hashicorp/hcl/v2"
+	appsv1 "k8s.io/api/apps/v1"
+	networkingv1beta1 "k8s.io/api/networking/v1beta1"
+	"sigs.k8s.io/yaml"
+
+	"github.com/kinvolk/lokomotive/pkg/components"
+	"github.com/kinvolk/lokomotive/pkg/components/util"
+)
+
+func renderManifests(configHCL string) (map[string]string, error) {
+	component, err := components.Get("headlamp")
+	if err != nil {
+		return nil, err
+	}
+
+	body, diagnostics := util.GetComponentBody(configHCL, "headlamp")
+	if diagnostics != nil {
+		return nil, fmt.Errorf("Getting component body: %v", diagnostics)
+	}
+
+	diagnostics = component.LoadConfig(body, &hcl.EvalContext{})
+	if diagnostics.HasErrors() {
+		return nil, fmt.Errorf("Valid config should not return an error, got: %s", diagnostics)
+	}
+
+	ret, err := component.RenderManifests()
+	if err != nil {
+		return nil, err
+	}
+
+	return ret, nil
+}
+
+func checkHeadlampDeploymentNotEmpty(t *testing.T, m map[string]string) {
+	dStr, ok := m["headlamp/templates/deployment.yaml"]
+	if !ok {
+		t.Fatalf("deployment config not found")
+	}
+
+	i := appsv1.Deployment{}
+	if err := yaml.Unmarshal([]byte(dStr), i); err != nil {
+		t.Fatalf("failed unmarshaling manifest: %v", err)
+	}
+}
+
+func ingressFromYAML(s string) (*networkingv1beta1.Ingress, error) {
+	i := &networkingv1beta1.Ingress{}
+	if err := yaml.Unmarshal([]byte(s), i); err != nil {
+		return nil, err
+	}
+
+	return i, nil
+}
+
+func TestRenderManifest(t *testing.T) { //nolint:funlen
+	type testCase struct {
+		name          string
+		configHCL     string
+		expectFailure bool
+		expectIngress string
+	}
+
+	tcs := []testCase{
+		{
+			"WithEmptyConfig",
+			`component "headlamp" {}`,
+			false,
+			``,
+		},
+		{
+			"WithEmptyIngress",
+			`
+			component "headlamp" {
+			  ingress {}
+			}`,
+			true,
+			``,
+		},
+		{
+			"WithMinimalIngressBlock",
+			`
+component "headlamp" {
+  ingress {
+    host = "headlamp.test.example.com"
+  }
+}
+`,
+			false,
+			`apiVersion: networking.k8s.io/v1beta1
+kind: Ingress
+metadata:
+  name: headlamp
+  labels:
+    helm.sh/chart: headlamp-0.1.0
+    app.kubernetes.io/name: headlamp
+    app.kubernetes.io/instance: headlamp
+    app.kubernetes.io/version: "0.0.0"
+    app.kubernetes.io/managed-by: Helm
+  annotations:
+    cert-manager.io/cluster-issuer: letsencrypt-production
+    contour.heptio.com/websocket-routes: /
+    kubernetes.io/ingress.class: contour
+spec:
+  tls:
+    - hosts:
+        - "headlamp.test.example.com"
+      secretName: headlamp.test.example.com-tls
+  rules:
+    - host: "headlamp.test.example.com"
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: headlamp
+              servicePort: 80`,
+		},
+		{
+			"WithAllParameters",
+			`
+			component "headlamp" {
+			  ingress {
+			    host = "headlamp.test.example.com"
+			    class = "nginx"
+			    certmanager_cluster_issuer = "letsencrypt-staging"
+			  }
+			}
+			`,
+			false,
+			`apiVersion: networking.k8s.io/v1beta1
+kind: Ingress
+metadata:
+  name: headlamp
+  labels:
+    helm.sh/chart: headlamp-0.1.0
+    app.kubernetes.io/name: headlamp
+    app.kubernetes.io/instance: headlamp
+    app.kubernetes.io/version: "0.0.0"
+    app.kubernetes.io/managed-by: Helm
+  annotations:
+    cert-manager.io/cluster-issuer: letsencrypt-staging
+    contour.heptio.com/websocket-routes: /
+    kubernetes.io/ingress.class: nginx
+spec:
+  tls:
+    - hosts:
+        - "headlamp.test.example.com"
+      secretName: headlamp.test.example.com-tls
+  rules:
+    - host: "headlamp.test.example.com"
+      http:
+        paths:
+          - path: /
+            backend:
+              serviceName: headlamp
+              servicePort: 80`,
+		},
+	}
+
+	testFunc := func(t *testing.T, tc testCase) {
+		m, err := renderManifests(tc.configHCL)
+		if err != nil {
+			if tc.expectFailure {
+				return
+			}
+
+			t.Fatalf("Rendering manifests: %v", err)
+		}
+
+		if len(m) == 0 {
+			t.Fatalf("Rendered manifests shouldn't be empty with valid config")
+		}
+
+		checkHeadlampDeploymentNotEmpty(t, m)
+
+		if tc.expectIngress == "" {
+			return
+		}
+
+		got, err := ingressFromYAML(m["headlamp/templates/ingress.yaml"])
+		if err != nil {
+			t.Fatalf("Unmarshaling ingress: %v", err)
+		}
+
+		want, err := ingressFromYAML(tc.expectIngress)
+		if err != nil {
+			t.Fatalf("Unmarshaling deployment: %v", err)
+		}
+
+		if diff := cmp.Diff(got, want); diff != "" {
+			t.Fatalf("unexpected deployment (-want +got)\n%s", diff)
+		}
+	}
+
+	for _, tc := range tcs {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			testFunc(t, tc)
+		})
+	}
+}
diff --git a/pkg/terraform/executor_test.go b/pkg/terraform/executor_test.go
index 3185eb35..8d5abed7 100644
--- a/pkg/terraform/executor_test.go
+++ b/pkg/terraform/executor_test.go
@@ -31,50 +31,21 @@ func TestVersionConstraint(t *testing.T) {
 		expectError bool
 	}{
 		"valid": {
-			output: `{
-  "terraform_version": "0.13.10",
-  "terraform_revision": "",
-  "provider_selections": {},
-  "terraform_outdated": false
-}
-`,
+			output: "Terraform v0.12.10",
 		},
 		"outdated": {
-			output: `{
-  "terraform_version": "0.11.0",
-  "terraform_revision": "",
-  "provider_selections": {},
-  "terraform_outdated": false
-}
-`,
+			output:      "Terraform v0.11.0",
 			expectError: true,
 		},
 		"unsupported": {
-			output: `{
-  "terraform_version": "0.14.5",
-  "terraform_revision": "",
-  "provider_selections": {},
-  "terraform_outdated": false
-}
-`,
+			output:      "Terraform v0.13.5",
 			expectError: true,
 		},
-		"not JSON": {
-			output: `Terraform v0.13.11
+		"with extra test": {
+			output: `Terraform v0.12.11
 
 Your version of Terraform is out of date! The latest version
 is 0.13.3. You can update by downloading from https://www.terraform.io/downloads.html`,
-			expectError: true,
-		},
-		"empty version field": {
-			output: `{
-  "terraform_version": "",
-  "terraform_revision": "",
-  "provider_selections": {},
-  "terraform_outdated": false
-}
-`,
-			expectError: true,
 		},
 	}
 
diff --git a/test/components/headlamp/headlamp_test.go b/test/components/headlamp/headlamp_test.go
new file mode 100644
index 00000000..34534f5b
--- /dev/null
+++ b/test/components/headlamp/headlamp_test.go
@@ -0,0 +1,35 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+// +build aws aws_edge packet aks
+// +build e2e
+
+package headlamp_test
+
+import (
+	"testing"
+
+	testutil "github.com/kinvolk/lokomotive/test/components/util"
+)
+
+const (
+	deploymentName = "headlamp"
+	namespace      = "lokomotive-system"
+)
+
+func TestHeadlampDeployment(t *testing.T) {
+	client := testutil.CreateKubeClient(t)
+
+	testutil.WaitForDeployment(t, client, namespace, deploymentName, testutil.RetryInterval, testutil.TimeoutSlow)
+}
diff --git a/test/ingress/aws/aws_test.go b/test/ingress/aws/aws_test.go
index 0362fa59..812f4d94 100644
--- a/test/ingress/aws/aws_test.go
+++ b/test/ingress/aws/aws_test.go
@@ -18,22 +18,9 @@
 package aws
 
 import (
-	"context"
-	"crypto/tls"
-	"crypto/x509"
-	"fmt"
-	"net/http"
 	"testing"
-	"time"
 
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/util/wait"
-
-	testutil "github.com/kinvolk/lokomotive/test/components/util"
-)
-
-const (
-	httpTimeout = 4 * time.Second
+	testingress "github.com/kinvolk/lokomotive/test/ingress"
 )
 
 func TestAWSIngress(t *testing.T) {
@@ -41,16 +28,19 @@ func TestAWSIngress(t *testing.T) {
 		Component string
 		Namespace string
 		Ingress   string
+		Subpath   string
 	}{
 		{
 			Component: "httpbin",
 			Namespace: "httpbin",
 			Ingress:   "httpbin",
+			Subpath:   "get",
 		},
 		{
-			Component: "promtheus-operator",
+			Component: "prometheus-operator",
 			Namespace: "monitoring",
 			Ingress:   "prometheus-operator-prometheus",
+			Subpath:   "graph",
 		},
 	}
 
@@ -58,89 +48,7 @@ func TestAWSIngress(t *testing.T) {
 		tc := tc
 		t.Run(tc.Ingress, func(t *testing.T) {
 			t.Parallel()
-
-			client := testutil.CreateKubeClient(t)
-
-			i, err := client.NetworkingV1beta1().Ingresses("httpbin").Get(context.TODO(), "httpbin", metav1.GetOptions{})
-			if err != nil {
-				t.Fatalf("getting httpbin ingress: %v", err)
-			}
-
-			h := i.Spec.Rules[0].Host
-			c := getHTTPClient()
-
-			err = wait.PollImmediate(testutil.RetryInterval, testutil.TimeoutSlow, func() (bool, error) {
-				resp, err := c.Get(fmt.Sprintf("https://%s/get", h))
-				if err != nil {
-					t.Logf("got an HTTP error: %v", err)
-					return false, nil
-				}
-
-				defer func() {
-					if err := resp.Body.Close(); err != nil {
-						t.Logf("closing HTTP response body: %v", err)
-					}
-				}()
-
-				if resp.StatusCode != http.StatusOK {
-					t.Logf("got a non-OK HTTP status: %d", resp.StatusCode)
-					return false, nil
-				}
-
-				return true, nil
-			})
-			if err != nil {
-				t.Fatal("could not get a successful HTTP response in time")
-			}
+			testingress.AccessIngress(t, tc.Namespace, tc.Ingress, tc.Subpath)
 		})
 	}
 }
-
-// getHTTPClient creates a HTTP client with LetsEncrypt Staging Root PEM certificate.
-func getHTTPClient() *http.Client {
-	// Get this Root PEM from https://letsencrypt.org/docs/staging-environment/#root-certificate
-	letsEncryptStagingRootPEM := `-----BEGIN CERTIFICATE-----
-MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw
-GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2
-MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq
-hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ
-diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP
-xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG
-TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj
-EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd
-O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa
-aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0
-A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr
-IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe
-Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb
-Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50
-qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB
-Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA
-A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln
-uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H
-sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm
-dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd
-oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV
-/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ
-zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc
-VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1
-Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4
-8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c
-idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng==
------END CERTIFICATE-----`
-
-	rootCAs := x509.NewCertPool()
-	if ok := rootCAs.AppendCertsFromPEM([]byte(letsEncryptStagingRootPEM)); !ok {
-		// This should fail in the developer testing itself.
-		panic("Failed to parse root certificate")
-	}
-
-	return &http.Client{
-		Timeout: httpTimeout,
-		Transport: &http.Transport{
-			TLSClientConfig: &tls.Config{
-				RootCAs: rootCAs,
-			},
-		},
-	}
-}
diff --git a/test/ingress/headlamp/ingress_test.go b/test/ingress/headlamp/ingress_test.go
new file mode 100644
index 00000000..7dd78ac2
--- /dev/null
+++ b/test/ingress/headlamp/ingress_test.go
@@ -0,0 +1,28 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+// +build aws aws_edge
+// +build e2e
+
+package headlamp_test
+
+import (
+	"testing"
+
+	testingress "github.com/kinvolk/lokomotive/test/ingress"
+)
+
+func TestHeadlampIngress(t *testing.T) {
+	testingress.AccessIngress(t, "lokomotive-system", "headlamp", "")
+}
diff --git a/test/ingress/ingress.go b/test/ingress/ingress.go
new file mode 100644
index 00000000..5ab8c55e
--- /dev/null
+++ b/test/ingress/ingress.go
@@ -0,0 +1,130 @@
+// Copyright 2020 The Lokomotive Authors
+//
+// 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.
+
+// Package ingress contains functions related to testing Ingress endpoints.
+package ingress
+
+import (
+	"context"
+	"crypto/tls"
+	"crypto/x509"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/wait"
+
+	testutil "github.com/kinvolk/lokomotive/test/components/util"
+)
+
+const (
+	httpTimeout = 4 * time.Second
+)
+
+// getHTTPClient creates a HTTP client with LetsEncrypt Staging Root PEM certificate.
+func getHTTPClient(timeout time.Duration) *http.Client {
+	// Get this Root PEM from https://letsencrypt.org/docs/staging-environment/#root-certificate
+	letsEncryptStagingRootPEM := `-----BEGIN CERTIFICATE-----
+MIIFATCCAumgAwIBAgIRAKc9ZKBASymy5TLOEp57N98wDQYJKoZIhvcNAQELBQAw
+GjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMB4XDTE2MDMyMzIyNTM0NloXDTM2
+MDMyMzIyNTM0NlowGjEYMBYGA1UEAwwPRmFrZSBMRSBSb290IFgxMIICIjANBgkq
+hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+pYHvQw5iU3v2b3iNuYNKYgsWD6KU7aJ
+diddtZQxSWYzUI3U0I1UsRPTxnhTifs/M9NW4ZlV13ZfB7APwC8oqKOIiwo7IwlP
+xg0VKgyz+kT8RJfYr66PPIYP0fpTeu42LpMJ+CKo9sbpgVNDZN2z/qiXrRNX/VtG
+TkPV7a44fZ5bHHVruAxvDnylpQxJobtCBWlJSsbIRGFHMc2z88eUz9NmIOWUKGGj
+EmP76x8OfRHpIpuxRSCjn0+i9+hR2siIOpcMOGd+40uVJxbRRP5ZXnUFa2fF5FWd
+O0u0RPI8HON0ovhrwPJY+4eWKkQzyC611oLPYGQ4EbifRsTsCxUZqyUuStGyp8oa
+aoSKfF6X0+KzGgwwnrjRTUpIl19A92KR0Noo6h622OX+4sZiO/JQdkuX5w/HupK0
+A0M0WSMCvU6GOhjGotmh2VTEJwHHY4+TUk0iQYRtv1crONklyZoAQPD76hCrC8Cr
+IbgsZLfTMC8TWUoMbyUDgvgYkHKMoPm0VGVVuwpRKJxv7+2wXO+pivrrUl2Q9fPe
+Kk055nJLMV9yPUdig8othUKrRfSxli946AEV1eEOhxddfEwBE3Lt2xn0hhiIedbb
+Ftf/5kEWFZkXyUmMJK8Ra76Kus2ABueUVEcZ48hrRr1Hf1N9n59VbTUaXgeiZA50
+qXf2bymE6F8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB
+Af8wHQYDVR0OBBYEFMEmdKSKRKDm+iAo2FwjmkWIGHngMA0GCSqGSIb3DQEBCwUA
+A4ICAQBCPw74M9X/Xx04K1VAES3ypgQYH5bf9FXVDrwhRFSVckria/7dMzoF5wln
+uq9NGsjkkkDg17AohcQdr8alH4LvPdxpKr3BjpvEcmbqF8xH+MbbeUEnmbSfLI8H
+sefuhXF9AF/9iYvpVNC8FmJ0OhiVv13VgMQw0CRKkbtjZBf8xaEhq/YqxWVsgOjm
+dm5CAQ2X0aX7502x8wYRgMnZhA5goC1zVWBVAi8yhhmlhhoDUfg17cXkmaJC5pDd
+oenZ9NVhW8eDb03MFCrWNvIh89DDeCGWuWfDltDq0n3owyL0IeSn7RfpSclpxVmV
+/53jkYjwIgxIG7Gsv0LKMbsf6QdBcTjhvfZyMIpBRkTe3zuHd2feKzY9lEkbRvRQ
+zbh4Ps5YBnG6CKJPTbe2hfi3nhnw/MyEmF3zb0hzvLWNrR9XW3ibb2oL3424XOwc
+VjrTSCLzO9Rv6s5wi03qoWvKAQQAElqTYRHhynJ3w6wuvKYF5zcZF3MDnrVGLbh1
+Q9ePRFBCiXOQ6wPLoUhrrbZ8LpFUFYDXHMtYM7P9sc9IAWoONXREJaO08zgFtMp4
+8iyIYUyQAbsvx8oD2M8kRvrIRSrRJSl6L957b4AFiLIQ/GgV2curs0jje7Edx34c
+idWw1VrejtwclobqNMVtG3EiPUIpJGpbMcJgbiLSmKkrvQtGng==
+-----END CERTIFICATE-----`
+
+	rootCAs := x509.NewCertPool()
+	if ok := rootCAs.AppendCertsFromPEM([]byte(letsEncryptStagingRootPEM)); !ok {
+		// This should fail in the developer testing itself.
+		panic("Failed to parse root certificate")
+	}
+
+	return &http.Client{
+		Timeout: timeout,
+		Transport: &http.Transport{
+			TLSClientConfig: &tls.Config{
+				MinVersion: tls.VersionTLS12,
+				RootCAs:    rootCAs,
+			},
+		},
+	}
+}
+
+// AccessIngress access the first host of Ingress name in namespace in subPath
+// expecting an http StatusOK.
+func AccessIngress(t *testing.T, namespace, name, subPath string) {
+	client := testutil.CreateKubeClient(t)
+
+	i, err := client.NetworkingV1beta1().Ingresses(namespace).Get(context.TODO(), name, metav1.GetOptions{})
+	if err != nil {
+		t.Fatalf("Getting %s ingress: %v", name, err)
+	}
+
+	h := i.Spec.Rules[0].Host
+	c := getHTTPClient(httpTimeout)
+
+	err = wait.PollImmediate(testutil.RetryInterval, testutil.TimeoutSlow, func() (bool, error) {
+		req, err := http.NewRequestWithContext(context.TODO(), "GET", (fmt.Sprintf("https://%s/%s", h, subPath)), nil)
+		if err != nil {
+			return false, fmt.Errorf("creating GET request: %w", err)
+		}
+
+		resp, err := c.Do(req)
+		if err != nil {
+			t.Logf("got an HTTP error: %v", err)
+
+			return false, nil
+		}
+
+		defer func() {
+			if err := resp.Body.Close(); err != nil {
+				t.Logf("closing HTTP response body: %v", err)
+			}
+		}()
+
+		if resp.StatusCode != http.StatusOK {
+			t.Logf("got a non-OK HTTP status: %d", resp.StatusCode)
+
+			return false, nil
+		}
+
+		return true, nil
+	})
+	if err != nil {
+		t.Fatalf("Could not get a successful HTTP response in time")
+	}
+}