diff --git a/.tool-versions b/.tool-versions index 83f1768..a208adf 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,6 @@ golang 1.22.1 make 4.4.1 goreleaser 1.24.0 +tilt 0.33.11 +ctlptl 0.8.28 +helm 3.14.3 diff --git a/Dockerfile-dev b/Dockerfile-dev index 0b13fe2..4f230d5 100644 --- a/Dockerfile-dev +++ b/Dockerfile-dev @@ -18,8 +18,11 @@ ENV CGO_ENABLED=0 WORKDIR /src -COPY . . +COPY ./cmd /src/cmd +COPY ./internal /src/internal +COPY ./static /src/static +COPY ./templates /src/templates +COPY ./go.mod /src/go.mod +COPY ./go.sum /src/go.sum RUN go mod download - -ENTRYPOINT [ "go", "run", "./cmd/gangplank" ] diff --git a/Makefile b/Makefile index f138b75..cc3cc9d 100644 --- a/Makefile +++ b/Makefile @@ -62,14 +62,8 @@ check-variable-%: .PHONY: license-check license-check: @addlicense -c "SIGHUP s.r.l" -y 2017-present -v -l apache \ - -ignore "deployments/helm/permission-monitor/templates/**/*" \ + -ignore "deployments/helm/templates/**/*" \ -ignore "dist/**/*" \ - -ignore "web-client/src/gen/**/*" \ - -ignore "web-client/node_modules/**/*" \ - -ignore "vendor/**/*" \ - -ignore "*.gen.go" \ - -ignore ".idea/*" \ - -ignore ".vscode/*" \ -ignore ".go/**/*" \ --check . @@ -80,14 +74,8 @@ license-check: .PHONY: license-add license-add: @addlicense -c "SIGHUP s.r.l" -y 2017-present -v -l apache \ - -ignore "deployments/helm/permission-monitor/templates/**/*" \ + -ignore "deployments/helm/templates/**/*" \ -ignore "dist/**/*" \ - -ignore "web-client/src/gen/**/*" \ - -ignore "web-client/node_modules/**/*" \ - -ignore "vendor/**/*" \ - -ignore "*.gen.go" \ - -ignore ".idea/*" \ - -ignore ".vscode/*" \ -ignore ".go/**/*" \ . diff --git a/Tiltfile b/Tiltfile index e69de29..382273d 100644 --- a/Tiltfile +++ b/Tiltfile @@ -0,0 +1,78 @@ +load("ext://helm_resource", "helm_repo", "helm_resource") +load("ext://restart_process", "docker_build_with_restart") +load("ext://namespace", "namespace_create") + +# Set default trigger mode to manual +trigger_mode(TRIGGER_MODE_MANUAL) + +# Disable analytics +analytics_settings(False) + +# Disable secrets scrubbing +secret_settings(disable_scrub=True) + +# Allow only gangplank k8s context +allow_k8s_contexts("kind-gangplank") + +# Create the namespaces +namespace_create("gangplank") +namespace_create("dex") + +k8s_resource( + new_name="namespaces", + objects=["gangplank:namespace","dex:namespace"], +) + +helm_repo( + name="dex", + url="https://charts.dexidp.io", + resource_name="dex-repo", +) + +helm_resource( + name="dex", + chart="dex/dex", + release_name="dex", + namespace="dex", + flags=["--values", "./configs/helm-values/dex.yaml", "--version=0.16.0"], + deps=["namespaces", "dex-repo"], +) + +k8s_resource( + workload="dex", + port_forwards=["5556:5556"], +) + +helm_resource( + name="gangplank", + chart="./deployments/helm", + release_name="gangplank", + image_deps=["gangplank"], + image_keys=[ + ("image.repository", "image.tag"), + ], + namespace="gangplank", + deps=["namespaces"], + flags=['--values', "./configs/helm-values/gangplank.yaml"], +) + +docker_build_with_restart( + ref="gangplank", + context=".", + dockerfile="Dockerfile-dev", + live_update=[ + sync("./cmd", "/src/cmd"), + sync("./internal", "/src/internal"), + sync("./static", "/src/static"), + sync("./templates", "/src/templates"), + sync("./go.mod", "/src/go.mod"), + sync("./go.sum", "/src/go.sum"), + ], + entrypoint=["go", "run", "./cmd/gangplank"], +) + +k8s_resource( + workload="gangplank", + port_forwards=["8080:8080"], + trigger_mode=TRIGGER_MODE_AUTO +) diff --git a/cmd/gangplank/main.go b/cmd/gangplank/main.go index 6f1e6af..d1f4cb8 100644 --- a/cmd/gangplank/main.go +++ b/cmd/gangplank/main.go @@ -30,6 +30,7 @@ import ( "github.com/sighupio/gangplank/internal/config" "github.com/sighupio/gangplank/internal/oidc" "github.com/sighupio/gangplank/internal/session" + "github.com/sighupio/gangplank/static" "golang.org/x/oauth2" ) @@ -90,6 +91,7 @@ func main() { loginRequiredHandlers := alice.New(loginRequired) http.HandleFunc(cfg.GetRootPathPrefix(), httpLogger(homeHandler)) + http.HandleFunc(fmt.Sprintf("%s/static/", cfg.HTTPPath), httpLogger(http.StripPrefix(fmt.Sprintf("%s/static/", cfg.HTTPPath), http.FileServerFS(static.FS)).ServeHTTP)) http.HandleFunc(fmt.Sprintf("%s/login", cfg.HTTPPath), httpLogger(loginHandler)) http.HandleFunc(fmt.Sprintf("%s/callback", cfg.HTTPPath), httpLogger(callbackHandler)) diff --git a/configs/cluster.yaml b/configs/cluster.yaml new file mode 100644 index 0000000..8d5e814 --- /dev/null +++ b/configs/cluster.yaml @@ -0,0 +1,23 @@ +# Copyright 2017-present SIGHUP s.r.l +# +# 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. + +apiVersion: ctlptl.dev/v1alpha1 +kind: Cluster +product: kind +registry: gangplank-registry +kindV1Alpha4Cluster: + name: gangplank + nodes: + - role: control-plane + image: kindest/node:v1.29.2 diff --git a/configs/helm-values/dex.yaml b/configs/helm-values/dex.yaml new file mode 100644 index 0000000..ec6b47d --- /dev/null +++ b/configs/helm-values/dex.yaml @@ -0,0 +1,33 @@ +# Copyright 2017-present SIGHUP s.r.l +# +# 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. + +config: + issuer: http://localhost:5556 + storage: + type: memory + enablePasswordDB: true + + staticClients: + - id: gangplank + secret: gangplank + name: Gangplank + redirectURIs: + - http://localhost:8080/callback + + staticPasswords: + - email: admin@gangplank.test + # bcrypt hash of the string "password": $(echo password | htpasswd -BinC 10 admin | cut -d: -f2) + hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" + username: admin + userID: 08a8684b-db88-4b73-90a9-3cd1661f5466 diff --git a/configs/helm-values/gangplank.yaml b/configs/helm-values/gangplank.yaml new file mode 100644 index 0000000..0eb1ff9 --- /dev/null +++ b/configs/helm-values/gangplank.yaml @@ -0,0 +1,25 @@ +# Copyright 2017-present SIGHUP s.r.l +# +# 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. + +command: + ["go", "run", "./cmd/gangplank", "-config", "/etc/gangplank/config.yaml"] + +config: + authorizeURL: http://localhost:5556/auth + tokenURL: http://dex.dex:5556/token + audience: http://dex.dex:5556/userinfo + redirectURL: http://localhost:8080/callback + usernameClaim: email + clientID: gangplank + clientSecret: gangplank diff --git a/deployments/helm/.helmignore b/deployments/helm/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/deployments/helm/.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/deployments/helm/Chart.yaml b/deployments/helm/Chart.yaml new file mode 100644 index 0000000..27c3b7a --- /dev/null +++ b/deployments/helm/Chart.yaml @@ -0,0 +1,20 @@ +# Copyright 2017-present SIGHUP s.r.l +# +# 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. + +apiVersion: v2 +name: gangplank +description: A Helm chart for Gangplank +type: application +version: 0.1.0 +appVersion: "0.1.0" diff --git a/deployments/helm/templates/_helpers.tpl b/deployments/helm/templates/_helpers.tpl new file mode 100644 index 0000000..dc5fcc4 --- /dev/null +++ b/deployments/helm/templates/_helpers.tpl @@ -0,0 +1,51 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "gangplank.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 "gangplank.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 "gangplank.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "gangplank.labels" -}} +helm.sh/chart: {{ include "gangplank.chart" . }} +{{ include "gangplank.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "gangplank.selectorLabels" -}} +app.kubernetes.io/name: {{ include "gangplank.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} diff --git a/deployments/helm/templates/configmap.yaml b/deployments/helm/templates/configmap.yaml new file mode 100644 index 0000000..3b01433 --- /dev/null +++ b/deployments/helm/templates/configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "gangplank.fullname" . }} + labels: + {{- include "gangplank.labels" . | nindent 4 }} +data: + config.yaml: | + {{- toYaml .Values.config | nindent 4 }} diff --git a/deployments/helm/templates/deployment.yaml b/deployments/helm/templates/deployment.yaml new file mode 100644 index 0000000..9531a8d --- /dev/null +++ b/deployments/helm/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "gangplank.fullname" . }} + labels: + {{- include "gangplank.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicas }} + selector: + matchLabels: + {{- include "gangplank.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "gangplank.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 }} + command: + {{- toYaml .Values.command | nindent 12 }} + envFrom: + - secretRef: + name: {{ include "gangplank.fullname" . }} + {{- if .Values.envs }} + env: + {{- range $key, $val := .Values.envs }} + - name: {{ $key }} + value: {{ $val | quote }} + {{- end }} + {{- end }} + ports: + - name: http + containerPort: 8080 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + volumeMounts: + - name: gangplank + mountPath: /etc/gangplank + readOnly: true + volumes: + - name: gangplank + configMap: + name: {{ include "gangplank.fullname" . }} + {{- 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/deployments/helm/templates/ingress.yaml b/deployments/helm/templates/ingress.yaml new file mode 100644 index 0000000..5d375e6 --- /dev/null +++ b/deployments/helm/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "gangplank.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else 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 "gangplank.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- 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: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deployments/helm/templates/secret.yaml b/deployments/helm/templates/secret.yaml new file mode 100644 index 0000000..62a5e54 --- /dev/null +++ b/deployments/helm/templates/secret.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "gangplank.fullname" . }} + labels: + {{- include "gangplank.labels" . | nindent 4 }} +data: + {{- range $key, $val := .Values.sensitiveEnvs }} + {{ $key }}: {{ $val | b64enc | quote }} + {{- end }} diff --git a/deployments/helm/templates/service.yaml b/deployments/helm/templates/service.yaml new file mode 100644 index 0000000..636fdbd --- /dev/null +++ b/deployments/helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "gangplank.fullname" . }} + labels: + {{- include "gangplank.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - name: http + port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + selector: + {{- include "gangplank.selectorLabels" . | nindent 4 }} diff --git a/deployments/helm/values.yaml b/deployments/helm/values.yaml new file mode 100644 index 0000000..9ba6c2a --- /dev/null +++ b/deployments/helm/values.yaml @@ -0,0 +1,139 @@ +# Copyright 2017-present SIGHUP s.r.l +# +# 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. + +image: + repository: registry.sighup.io/fury/gangplank + pullPolicy: IfNotPresent + tag: "" + +replicas: 1 +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +podAnnotations: {} +podSecurityContext: {} +securityContext: {} +resources: {} +nodeSelector: {} +tolerations: [] +affinity: {} +command: ["gangplank", "-config", "/etc/gangplank/config.yaml"] + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + hosts: + - host: gangplank.example.test + paths: + - path: / + pathType: Prefix + tls: [] + +envs: {} +sensitiveEnvs: + GANGPLANK_CONFIG_SESSION_SECURITY_KEY: session-security-key +config: + # The address to listen on. Defaults to 0.0.0.0 to listen on all interfaces. + # Env var: GANGPLANK_CONFIG_HOST + # host: 0.0.0.0 + + # The port to listen on. Defaults to 8080. + # Env var: GANGPLANK_CONFIG_PORT + # port: 8080 + + # Should Gangplank serve TLS vs. plain HTTP? Default: false + # Env var: GANGPLANK_CONFIG_SERVE_TLS + # serveTLS: false + + # The public cert file (including root and intermediates) to use when serving + # TLS. + # Env var: GANGPLANK_CONFIG_CERT_FILE + # certFile: /etc/gangplank/tls.crt + + # The private key file when serving TLS. + # Env var: GANGPLANK_CONFIG_KEY_FILE + # keyFile: /etc/gangplank/tls.key + + # The cluster name. Used in UI and kubectl config instructions. + # Env var: GANGPLANK_CONFIG_CLUSTER_NAME + clusterName: "cluster-name" + + # OAuth2 URL to start authorization flow. + # Env var: GANGPLANK_CONFIG_AUTHORIZE_URL + authorizeURL: "https://oauth2provider.test/authorize" + + # OAuth2 URL to obtain access tokens. + # Env var: GANGPLANK_CONFIG_TOKEN_URL + tokenURL: "https://oauth2provider.test/token" + + # Endpoint that provides user profile information [optional]. Not all providers + # will require this. + # Env var: GANGPLANK_CONFIG_AUDIENCE + # audience: "https://oauth2provider.test/audience" + + # Used to specify the scope of the requested Oauth authorization. + # scopes: ["openid", "profile", "email", "offline_access"] + + # Where to redirect back to. This should be a URL where gangplank is reachable. + # Typically this also needs to be registered as part of the oauth application + # with the oAuth provider. + # Env var: GANGPLANK_CONFIG_REDIRECT_URL + redirectURL: "https://gangplank.example.test/callback" + + # API client ID as indicated by the identity provider + # Env var: GANGPLANK_CONFIG_CLIENT_ID + clientID: "client-id" + + # API client secret as indicated by the identity provider + # Env var: GANGPLANK_CONFIG_CLIENT_SECRET + clientSecret: "client-secret" + + # Some identity providers accept an empty client secret, this + # is not generally considered a good idea. If you have to use an + # empty secret and accept the risks that come with that then you can + # set this to true. + # allowEmptyClientSecret: false + + # The JWT claim to use as the username. This is used in UI. + # Default is "nickname". This is combined with the clusterName + # for the "user" portion of the kubeconfig. + # Env var: GANGPLANK_CONFIG_USERNAME_CLAIM + # usernameClaim: "sub" + + # The API server endpoint used to configure kubectl + # Env var: GANGPLANK_CONFIG_APISERVER_URL + apiServerURL: "https://apiserver.example.test" + + # The path to find the CA bundle for the API server. Used to configure kubectl. + # This is typically mounted into the default location for workloads running on + # a Kubernetes cluster and doesn't need to be set. + # Env var: GANGPLANK_CONFIG_CLUSTER_CA_PATH + # clusterCAPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" + + # The path to a root CA to trust for self signed certificates at the Oauth2 URLs + # Env var: GANGPLANK_CONFIG_TRUSTED_CA_PATH + # trustedCAPath: /cacerts/rootca.crt + + # The path gangplank uses to create urls (defaults to "") + # Env var: GANGPLANK_CONFIG_HTTP_PATH + # httpPath: "https://gangplank.example.test" + + # The path to find custom HTML templates + # Env var: GANGPLANK_CONFIG_CUSTOM_HTTP_TEMPLATES_DIR + # customHTMLTemplatesDir: /custom-templates diff --git a/docs/README.md b/docs/README.md index 8f85f84..2b9fe82 100644 --- a/docs/README.md +++ b/docs/README.md @@ -29,7 +29,7 @@ kubectl -n gangplank create secret generic gangplank-key \ Gangplank takes an optional path prefix if you want to host it at a url other than '/' (e.g. `https://example.com/gangplank`). By configuring this parameter, all redirects will have the proper path appended to the url parameters. -This variable can be configured via the [ConfigMap](https://github.com/sighup/gangplank/blob/master/docs/yaml/02-config.yaml#L81) or via environment variable (`GANGPLANK_HTTP_PATH`). +This variable can be configured via the [ConfigMap](https://github.com/sighup/gangplank/blob/master/docs/yaml/02-config.yaml#L81) or via environment variable (`GANGPLANK_CONFIG_HTTP_PATH`). ## Detailed Instructions diff --git a/docs/yaml/02-config.yaml b/docs/yaml/02-config.yaml index e9b2a48..7628863 100644 --- a/docs/yaml/02-config.yaml +++ b/docs/yaml/02-config.yaml @@ -20,41 +20,41 @@ metadata: data: gangplank.yaml: | # The address to listen on. Defaults to 0.0.0.0 to listen on all interfaces. - # Env var: GANGPLANK_HOST + # Env var: GANGPLANK_CONFIG_HOST # host: 0.0.0.0 # The port to listen on. Defaults to 8080. - # Env var: GANGPLANK_PORT + # Env var: GANGPLANK_CONFIG_PORT # port: 8080 # Should Gangplank serve TLS vs. plain HTTP? Default: false - # Env var: GANGPLANK_SERVE_TLS + # Env var: GANGPLANK_CONFIG_SERVE_TLS # serveTLS: false # The public cert file (including root and intermediates) to use when serving # TLS. - # Env var: GANGPLANK_CERT_FILE + # Env var: GANGPLANK_CONFIG_CERT_FILE # certFile: /etc/gangplank/tls/tls.crt # The private key file when serving TLS. - # Env var: GANGPLANK_KEY_FILE + # Env var: GANGPLANK_CONFIG_KEY_FILE # keyFile: /etc/gangplank/tls/tls.key # The cluster name. Used in UI and kubectl config instructions. - # Env var: GANGPLANK_CLUSTER_NAME - clusterName: "${GANGPLANK_CLUSTER_NAME}" + # Env var: GANGPLANK_CONFIG_CLUSTER_NAME + clusterName: "${GANGPLANK_CONFIG_CLUSTER_NAME}" # OAuth2 URL to start authorization flow. - # Env var: GANGPLANK_AUTHORIZE_URL + # Env var: GANGPLANK_CONFIG_AUTHORIZE_URL authorizeURL: "https://${DNS_NAME}/authorize" # OAuth2 URL to obtain access tokens. - # Env var: GANGPLANK_TOKEN_URL + # Env var: GANGPLANK_CONFIG_TOKEN_URL tokenURL: "https://${DNS_NAME}/oauth/token" # Endpoint that provides user profile information [optional]. Not all providers # will require this. - # Env var: GANGPLANK_AUDIENCE + # Env var: GANGPLANK_CONFIG_AUDIENCE audience: "https://${DNS_NAME}/userinfo" # Used to specify the scope of the requested Oauth authorization. @@ -63,16 +63,16 @@ data: # Where to redirect back to. This should be a URL where gangplank is reachable. # Typically this also needs to be registered as part of the oauth application # with the oAuth provider. - # Env var: GANGPLANK_REDIRECT_URL - redirectURL: "https://${GANGPLANK_REDIRECT_URL}/callback" + # Env var: GANGPLANK_CONFIG_REDIRECT_URL + redirectURL: "https://${GANGPLANK_CONFIG_REDIRECT_URL}/callback" # API client ID as indicated by the identity provider - # Env var: GANGPLANK_CLIENT_ID - clientID: "${GANGPLANK_CLIENT_ID}" + # Env var: GANGPLANK_CONFIG_CLIENT_ID + clientID: "${GANGPLANK_CONFIG_CLIENT_ID}" # API client secret as indicated by the identity provider - # Env var: GANGPLANK_CLIENT_SECRET - clientSecret: "${GANGPLANK_CLIENT_SECRET}" + # Env var: GANGPLANK_CONFIG_CLIENT_SECRET + clientSecret: "${GANGPLANK_CONFIG_CLIENT_SECRET}" # Some identity providers accept an empty client secret, this # is not generally considered a good idea. If you have to use an @@ -83,27 +83,27 @@ data: # The JWT claim to use as the username. This is used in UI. # Default is "nickname". This is combined with the clusterName # for the "user" portion of the kubeconfig. - # Env var: GANGPLANK_USERNAME_CLAIM + # Env var: GANGPLANK_CONFIG_USERNAME_CLAIM usernameClaim: "sub" # The API server endpoint used to configure kubectl - # Env var: GANGPLANK_APISERVER_URL - apiServerURL: "https://${GANGPLANK_APISERVER_URL}" + # Env var: GANGPLANK_CONFIG_APISERVER_URL + apiServerURL: "https://${GANGPLANK_CONFIG_APISERVER_URL}" # The path to find the CA bundle for the API server. Used to configure kubectl. # This is typically mounted into the default location for workloads running on # a Kubernetes cluster and doesn't need to be set. - # Env var: GANGPLANK_CLUSTER_CA_PATH + # Env var: GANGPLANK_CONFIG_CLUSTER_CA_PATH # clusterCAPath: "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt" # The path to a root CA to trust for self signed certificates at the Oauth2 URLs - # Env var: GANGPLANK_TRUSTED_CA_PATH + # Env var: GANGPLANK_CONFIG_TRUSTED_CA_PATH #trustedCAPath: /cacerts/rootca.crt # The path gangplank uses to create urls (defaults to "") - # Env var: GANGPLANK_HTTP_PATH - #httpPath: "https://${GANGPLANK_HTTP_PATH}" + # Env var: GANGPLANK_CONFIG_HTTP_PATH + #httpPath: "https://${GANGPLANK_CONFIG_HTTP_PATH}" # The path to find custom HTML templates - # Env var: GANGPLANK_CUSTOM_HTTP_TEMPLATES_DIR + # Env var: GANGPLANK_CONFIG_CUSTOM_HTTP_TEMPLATES_DIR #customHTMLTemplatesDir: /custom-templates diff --git a/docs/yaml/03-deployment.yaml b/docs/yaml/03-deployment.yaml index cbc73dc..88c765f 100644 --- a/docs/yaml/03-deployment.yaml +++ b/docs/yaml/03-deployment.yaml @@ -37,7 +37,7 @@ spec: imagePullPolicy: Always command: ["gangplank", "-config", "/gangplank/gangplank.yaml"] env: - - name: GANGPLANK_SESSION_SECURITY_KEY + - name: GANGPLANK_CONFIG_SESSION_SECURITY_KEY valueFrom: secretKeyRef: name: gangplank-key diff --git a/docs/yaml/05-ingress.yaml b/docs/yaml/05-ingress.yaml index d7d1fc1..2abca72 100644 --- a/docs/yaml/05-ingress.yaml +++ b/docs/yaml/05-ingress.yaml @@ -24,9 +24,9 @@ spec: tls: - secretName: gangplank hosts: - - ${GANGPLANK_HOST} + - ${GANGPLANK_CONFIG_HOST} rules: - - host: ${GANGPLANK_HOST} + - host: ${GANGPLANK_CONFIG_HOST} http: paths: - backend: diff --git a/internal/config/config.go b/internal/config/config.go index ea22737..42748c2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -80,7 +80,7 @@ func NewConfig(configFile string) (*Config, error) { } } - err := envconfig.Process("gangplank", cfg) + err := envconfig.Process("gangplank_config", cfg) if err != nil { return nil, err } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 47f18e9..cad56a9 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -27,17 +27,17 @@ func TestConfigNotFound(t *testing.T) { } func TestEnvionmentOverrides(t *testing.T) { - os.Setenv("GANGPLANK_AUTHORIZE_URL", "https://foo.bar/authorize") - os.Setenv("GANGPLANK_APISERVER_URL", "https://k8s-api.foo.baz") - os.Setenv("GANGPLANK_CLIENT_ID", "foo") - os.Setenv("GANGPLANK_CLIENT_SECRET", "bar") - os.Setenv("GANGPLANK_PORT", "1234") - os.Setenv("GANGPLANK_REDIRECT_URL", "https://foo.baz/callback") - os.Setenv("GANGPLANK_CLUSTER_CA_PATH", "/etc/ssl/certs/ca-certificates.crt") - os.Setenv("GANGPLANK_SESSION_SECURITY_KEY", "testing") - os.Setenv("GANGPLANK_TOKEN_URL", "https://foo.bar/token") - os.Setenv("GANGPLANK_AUDIENCE", "foo") - os.Setenv("GANGPLANK_SCOPES", "groups,sub") + os.Setenv("GANGPLANK_CONFIG_AUTHORIZE_URL", "https://foo.bar/authorize") + os.Setenv("GANGPLANK_CONFIG_APISERVER_URL", "https://k8s-api.foo.baz") + os.Setenv("GANGPLANK_CONFIG_CLIENT_ID", "foo") + os.Setenv("GANGPLANK_CONFIG_CLIENT_SECRET", "bar") + os.Setenv("GANGPLANK_CONFIG_PORT", "1234") + os.Setenv("GANGPLANK_CONFIG_REDIRECT_URL", "https://foo.baz/callback") + os.Setenv("GANGPLANK_CONFIG_CLUSTER_CA_PATH", "/etc/ssl/certs/ca-certificates.crt") + os.Setenv("GANGPLANK_CONFIG_SESSION_SECURITY_KEY", "testing") + os.Setenv("GANGPLANK_CONFIG_TOKEN_URL", "https://foo.bar/token") + os.Setenv("GANGPLANK_CONFIG_AUDIENCE", "foo") + os.Setenv("GANGPLANK_CONFIG_SCOPES", "groups,sub") cfg, err := NewConfig("") if err != nil { t.Errorf("Failed to test config overrides with error: %s", err) diff --git a/static/static.go b/static/static.go new file mode 100644 index 0000000..5370b99 --- /dev/null +++ b/static/static.go @@ -0,0 +1,20 @@ +// Copyright 2017-present SIGHUP s.r.l +// +// 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 static + +import "embed" + +//go:embed *.css +var FS embed.FS diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..b66091f --- /dev/null +++ b/static/style.css @@ -0,0 +1,465 @@ +/** + * Copyright 2017-present SIGHUP s.r.l + * + * 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. + */ + +::selection { + background: rgba(54, 162, 239, 0.2); +} + +html::-webkit-scrollbar-corner, +html::-webkit-scrollbar-track { + background-color: #242529; +} + +html::-webkit-scrollbar-thumb { + background-color: rgba(152, 162, 179, 0.5); + background-clip: content-box; + border-radius: 16px; + border: 4px solid #242529; +} + +html::-webkit-scrollbar { + inline-size: 16px; + block-size: 16px; +} + +html, +body, +div, +span, +applet, +object, +iframe, +h1, +h2, +h3, +h4, +h5, +h6, +p, +blockquote, +pre, +a, +abbr, +acronym, +address, +big, +cite, +code, +del, +dfn, +em, +img, +ins, +kbd, +q, +s, +samp, +small, +strike, +strong, +sub, +sup, +tt, +var, +b, +u, +i, +center, +dl, +dt, +dd, +ol, +ul, +li, +fieldset, +form, +label, +legend, +table, +caption, +tbody, +tfoot, +thead, +tr, +th, +td, +article, +aside, +canvas, +details, +embed, +figure, +figcaption, +footer, +header, +hgroup, +menu, +nav, +output, +ruby, +section, +summary, +time, +mark, +audio, +video { + margin: 0; + padding: 0; + border: none; + vertical-align: baseline; +} + +html { + scrollbar-width: auto; + scrollbar-color: rgba(152, 162, 179, 0.5) #242529; + font-family: "Inter", BlinkMacSystemFont, Helvetica, Arial, sans-serif; + font-size: 0.875rem; + line-height: 1.1428571428571428; + font-weight: 400; + -webkit-text-size-adjust: 100%; + text-size-adjust: 100%; + font-kerning: normal; + block-size: 100%; + background-color: #141519; + color: #dfe5ef; +} + +*, +*:before, +*:after { + box-sizing: border-box; +} + +.euiFlexGroup-l-flexStart-stretch-column { + display: flex; + align-items: stretch; + flex-grow: 1; + gap: 24px; + justify-content: flex-start; + align-items: stretch; + flex-direction: column; +} + +.euiFlexItem-growZero { + display: flex; + flex-direction: column; + flex-grow: 0; + flex-basis: auto; +} + +.euiHeader-static-default { + display: flex; + justify-content: space-between; + block-size: 48px; + padding-inline: 8px; + box-shadow: 0 0.7px 1.4px rgba(0, 0, 0, 0.24500000000000002), + 0 1.9px 4px rgba(0, 0, 0, 0.17500000000000002), + 0 4.5px 10px rgba(0, 0, 0, 0.17500000000000002); + z-index: 999; + position: relative; + background-color: #1d1e24; + border-block-end: 1px solid #131417; +} + +.euiFlexGroup-l-center-center-column { + display: flex; + align-items: stretch; + flex-grow: 1; + gap: 24px; + justify-content: center; + align-items: center; + flex-direction: column; +} + +.euiText-m { + color: inherit; + font-weight: 400; + clear: both; + font-size: 1.1429rem; + line-height: 1.7143rem; +} + +.euiText-m > :last-child { + margin-block-end: 0 !important; +} + +.euiText-m h1 { + font-size: 2.4286rem; + line-height: 2.8571rem; +} + +.euiText-m h1, +.euiText-m h2, +.euiText-m h3, +.euiText-m h4, +.euiText-m h5, +.euiText-m h6, +.euiText-m dt { + color: inherit; +} + +.euiText-m h1 { + font-size: 2.4286rem; + line-height: 2.8571rem; + font-weight: 700; + color: #dfe5ef; +} + +h1, +h2, +h3, +h4, +h5, +h6, +p { + font-family: inherit; + font-weight: inherit; + font-size: inherit; +} + +.euiFlexItem-grow-1 { + display: flex; + flex-direction: column; + flex-basis: 0%; + flex-grow: 1; +} + +.euiText-m-euiTextAlign-center { + color: inherit; + font-weight: 400; + clear: both; + font-size: 1.1429rem; + line-height: 1.7143rem; + text-align: center; +} + +.euiText-m-euiTextAlign-center h1:not(:last-child) { + margin-block-end: 12px; +} + +.euiText-m-euiTextAlign-center h1 { + font-size: 2.4286rem; + line-height: 2.8571rem; +} + +.euiText-m-euiTextAlign-center h1, +.euiText-m-euiTextAlign-center h2, +.euiText-m-euiTextAlign-center h3, +.euiText-m-euiTextAlign-center h4, +.euiText-m-euiTextAlign-center h5, +.euiText-m-euiTextAlign-center h6, +.euiText-m-euiTextAlign-center dt { + color: inherit; +} + +.euiText-m-euiTextAlign-center h1 { + font-size: 2.4286rem; + line-height: 2.8571rem; + font-weight: 700; + color: #dfe5ef; +} + +.euiText-m-euiTextAlign-center > :last-child { + margin-block-end: 0 !important; +} + +.euiText-m-euiTextAlign-center p, +.euiText-m-euiTextAlign-center dl, +.euiText-m-euiTextAlign-center blockquote, +.euiText-m-euiTextAlign-center pre, +.euiText-m-euiTextAlign-center > ul, +.euiText-m-euiTextAlign-center > ol { + margin-block-end: 1.7143rem; +} + +a[href], +button, +[role="button"] { + cursor: pointer; +} + +a, +a:hover, +a:focus { + -webkit-text-decoration: none; + text-decoration: none; +} + +a { + color: #36a2ef; +} + +.euiButtonDisplay-m-defaultMinWidth-base-primary { + display: inline-block; + appearance: none; + cursor: pointer; + white-space: nowrap; + max-inline-size: 100%; + vertical-align: middle; + font-weight: 500; + padding-block: 0; + padding-inline: 12px; + block-size: 40px; + line-height: 40px; + font-size: 1rem; + line-height: 1.4286rem; + border-radius: 6px; + min-inline-size: 112px; + color: #36a2ef; + background-color: #103148; +} + +.euiButtonDisplay-m-defaultMinWidth-base-primary { + transition: transform 250ms ease-in-out, background-color 250ms ease-in-out; +} + +.euiButtonDisplayContent { + block-size: 100%; + inline-size: 100%; + display: flex; + justify-content: center; + align-items: center; + vertical-align: middle; + gap: 8px; +} + +.eui-textTruncate { + max-inline-size: 100%; + overflow: hidden !important; + text-overflow: ellipsis !important; + white-space: nowrap !important; +} + +.euiButtonDisplay-m-defaultMinWidth-base-primary:hover:not(:disabled) { + transform: translateY(-1px); +} + +.euiButtonDisplay-m-defaultMinWidth-base-primary:hover:not(:disabled), +.euiButtonDisplay-m-defaultMinWidth-base-primary:focus { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.euiFlexGroup-l-center-stretch-column { + display: flex; + align-items: stretch; + flex-grow: 1; + gap: 24px; + justify-content: center; + align-items: stretch; + flex-direction: column; +} + +.euiHeaderSectionItem { + position: relative; + display: flex; + align-items: center; +} + +.euiHeaderLinks { + display: flex; +} + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +menu, +nav, +section { + display: block; +} + +.euiHeaderLinks__list-s-EuiHeaderLinks { + white-space: nowrap; + display: flex; + align-items: center; + gap: 12px; +} + +.euiButtonDisplay-euiButtonEmpty-m-empty-text { + transition-timing-function: ease-in; + transition-duration: 150ms; +} + +.euiButtonDisplay-euiButtonEmpty-m-empty-text { + display: inline-block; + appearance: none; + cursor: pointer; + white-space: nowrap; + max-inline-size: 100%; + vertical-align: middle; + font-weight: 500; + padding-block: 0; + padding-inline: 12px; + padding-block: 0; + padding-inline: 8px; + block-size: 40px; + line-height: 40px; + font-size: 1rem; + line-height: 1.4286rem; + border-radius: 6px; + color: #dfe5ef; +} + +.euiButtonDisplay-euiButtonEmpty-m-empty-text:hover:not(:disabled), +.euiButtonDisplay-euiButtonEmpty-m-empty-text:focus { + -webkit-text-decoration: underline; + text-decoration: underline; +} + +.euiCodeBlock-m { + max-inline-size: 100%; + display: block; + position: relative; + background: #25262e; + font-size: 1rem; + line-height: 1.4286rem; +} + +.euiCodeBlock__pre-preWrap-padding { + block-size: 100%; + overflow: auto; + display: flex; + scrollbar-width: thin; + scrollbar-color: rgba(152, 162, 179, 0.5) transparent; + white-space: pre-wrap; + padding: 16px; +} + +code, +pre, +kbd, +samp { + font-family: "Roboto Mono", Menlo, Courier, monospace; +} + +.euiCodeBlock__code { + font-family: "Roboto Mono", Menlo, Courier, monospace; + font-size: inherit; + color: #dfe5ef; + display: flex; + flex-direction: column; +} + +.euiCodeBlock__line { + display: block; + margin: 8px 0; +} diff --git a/templates/commandline.tmpl b/templates/commandline.tmpl index 89bd047..43a88d1 100644 --- a/templates/commandline.tmpl +++ b/templates/commandline.tmpl @@ -1,107 +1,113 @@ - - - - - Gangplank - - - - - - - - - - - - - - - - - - -
-

- Welcome {{ .Username }}. -

-
- Claims received from the upstream issuer: -
-

-{{- range $key, $value := .Claims -}}
-    {{- if eq $key "groups" -}}
-        {{- printf "%s:\n" $key -}}
-        {{- range $groupName := $value -}}
-            {{- printf "- \"%s\"\n" $groupName -}}
-        {{- end -}}
-    {{- else -}}
-        {{- if eq (printf "%T" $value) "string" -}}
-            {{- printf "%s: \"%s\"\n" $key $value -}}
-        {{- else if eq (printf "%T" $value) "bool" -}}
-            {{- if eq $value true -}}
-                {{- printf "%s: true\n" $key -}}
-            {{- else -}}
-                {{- printf "%s: false\n" $key -}}
-            {{ end }}
-        {{- else if or (eq (printf "%T" $value) "float64") (eq (printf "%T" $value) "float32") -}}
-            {{- printf "%s: %f\n" $key $value -}}
-        {{- else -}}
-            {{- printf "%s: %d\n" $key $value -}}
-        {{- end -}}
-    {{- end -}}
-{{- end -}}
-            
-
- In order to get command-line access to the {{ .ClusterName }} Kubernetes cluster, you will need to configure OpenID Connect (OIDC) authentication for your client. -
-
-

- The Kubernetes command-line utility, kubectl, may be installed like so: -

-
-             
-$ curl -LO https://storage.googleapis.com/kubernetes-release/release/`curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt`/bin/$(uname | awk '{print tolower($0)}')/amd64/kubectl
-$ chmod +x ./kubectl
-$ sudo mv ./kubectl /usr/local/bin/kubectl
-             
-           
-
- -
Once kubectl is installed, you may execute the following:
+
+
+ + + Download Kubeconfig + + +
+
+
+

Or you can execute the following commands:

-
-               
-echo "{{ .ClusterCA }}" \ > "ca-{{ .ClusterName }}.pem"
-kubectl config set-cluster "{{ .ClusterName }}" --server={{ .APIServerURL }} --certificate-authority="ca-{{ .ClusterName }}.pem" --embed-certs
-kubectl config set-credentials "{{ .KubeCfgUser }}"  \
+          
+
+
+
+                
+                  echo "{{ .ClusterCA }}" \ > "ca-{{ .ClusterName }}.pem"
+                  kubectl config set-cluster "{{ .ClusterName }}" --server={{ .APIServerURL }} --certificate-authority="ca-{{ .ClusterName }}.pem" --embed-certs
+                  kubectl config set-credentials "{{ .KubeCfgUser }}"  \
     --auth-provider=oidc  \
     --auth-provider-arg='idp-issuer-url={{ .IssuerURL }}'  \
     --auth-provider-arg='client-id={{ .ClientID }}'  \
     --auth-provider-arg='client-secret={{ .ClientSecret }}' \
     --auth-provider-arg='refresh-token={{ .RefreshToken }}' \
-    --auth-provider-arg='id-token={{ .IDToken }}'
-kubectl config set-context "{{ .ClusterName }}" --cluster="{{ .ClusterName }}" --user="{{ .KubeCfgUser }}"
-kubectl config use-context "{{ .ClusterName }}"
-rm "ca-{{ .ClusterName }}.pem"
-              
-            
+ --auth-provider-arg='id-token={{ .IDToken }}' + kubectl config set-context "{{ .ClusterName }}" --cluster="{{ .ClusterName }}" --user="{{ .KubeCfgUser }}" + kubectl config use-context "{{ .ClusterName }}" + rm "ca-{{ .ClusterName }}.pem" +
+ +
+
- + + + diff --git a/templates/home.tmpl b/templates/home.tmpl index 56c62dc..5f306cf 100644 --- a/templates/home.tmpl +++ b/templates/home.tmpl @@ -1,65 +1,65 @@ - - - - - Heptio Gangplank - - - - - - - - -
-
-

-

Heptio Gangplank Kubernetes Authentication

-
-
This utility will help you authenticate with your Kubernetes cluster with an OpenID Connect (OIDC) flow. Sign in to get started.
+ + + + + Gangplank + + + + + +
+
+
+
+
+
+

Gangplank

+
+
+
+
-
- Sign In +
+
+
+
+

SIGHUP Gangplank Kubernetes Authentication

+

+ This utility will help you authenticate with your Kubernetes + cluster with an OpenID Connect (OIDC) flow. Sign in to get + started. +

+
+
+ +
-

-
-
- - - - - - -