diff --git a/assets/charts/components/headlamp/.helmignore b/assets/charts/components/headlamp/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /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 000000000..503aaf3b5 --- /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 000000000..47e103c53 --- /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 000000000..5c91e6bac --- /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 000000000..bd0785442 --- /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 000000000..00c7ff185 --- /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 000000000..15495e856 --- /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/ci/aks/aks-cluster.lokocfg.envsubst b/ci/aks/aks-cluster.lokocfg.envsubst index 204318400..3612025b1 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 ebd15b593..a94df0ca9 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 966e71e93..720208a56 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/cli/cmd/component.go b/cli/cmd/component.go index 9eeec84fe..fea271439 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/concepts/components.md b/docs/concepts/components.md index 591570243..bd231b223 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 000000000..8982fe429 --- /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/go.mod b/go.mod index 80ee601c8..547bf885d 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/generated_assets.go b/pkg/assets/generated_assets.go index bd2c5751e..8a50af58c 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), @@ -6120,6 +6177,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 +6376,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/components/headlamp/component.go b/pkg/components/headlamp/component.go new file mode 100644 index 000000000..9efc3429a --- /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/component_test.go b/pkg/components/headlamp/component_test.go new file mode 100644 index 000000000..64345571d --- /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/components/headlamp/doc.go b/pkg/components/headlamp/doc.go new file mode 100644 index 000000000..7fbd2d8f0 --- /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 000000000..4bc4c2414 --- /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/test/components/headlamp/headlamp_test.go b/test/components/headlamp/headlamp_test.go new file mode 100644 index 000000000..34534f5bf --- /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 0362fa591..812f4d94b 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 000000000..7dd78ac2d --- /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 000000000..5ab8c55e4 --- /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") + } +}