From 723b1e3627f0c63832a6f08acfb4d49a5fe05a36 Mon Sep 17 00:00:00 2001 From: Ivo Gosemann Date: Wed, 9 Oct 2024 18:24:05 +0200 Subject: [PATCH 1/2] refactor(headscale): removal of headscale Currently headscale is not used beeing deployed & used with Greenhouse. To avoid unnecessary refactorings and time spend to validate Dependabot PRs we decided to not touch the code for now. This PR removes all usage of Headscale, so that we can revert when we have a concrete usage for this in the future. --- .github/labeler.yml | 7 - .github/licenserc.yaml | 2 - .github/workflows/docker-build.yaml | 12 - .github/workflows/pr-docker-build.yaml | 6 - Dockerfile.headscalectl | 26 - Dockerfile.tailscale | 30 - Dockerfile.tcp-proxy | 27 - charts/greenhouse/Chart.lock | 10 +- charts/greenhouse/Chart.yaml | 10 +- charts/greenhouse/ci/test-values.yaml | 6 - charts/headscale/.helmignore | 23 - charts/headscale/Chart.yaml | 9 - charts/headscale/ci/test-values.yaml | 23 - charts/headscale/templates/_helpers.tpl | 84 -- charts/headscale/templates/deployment.yaml | 122 -- .../headscale/templates/etc/_acl_policy.yaml | 45 - charts/headscale/templates/etc/_config.yaml | 129 -- charts/headscale/templates/etc/_derp.yaml | 8 - charts/headscale/templates/etc/configmap.yaml | 23 - charts/headscale/templates/ingress-grpc.yaml | 35 - charts/headscale/templates/ingress.yaml | 46 - .../headscale/templates/postgres-service.yaml | 18 - .../templates/postgres-statefulset.yaml | 58 - charts/headscale/templates/pvc.yaml | 18 - charts/headscale/templates/rbac.yaml | 34 - charts/headscale/templates/secret.yaml | 16 - charts/headscale/templates/service.yaml | 26 - .../headscale/templates/serviceaccount.yaml | 11 - charts/headscale/values.yaml | 98 -- .../manager/crds/greenhouse.sap_clusters.yaml | 48 - charts/manager/templates/deployment.yaml | 11 - charts/manager/values.yaml | 7 - charts/tailscale-proxy/.helmignore | 23 - charts/tailscale-proxy/Chart.yaml | 27 - charts/tailscale-proxy/templates/_helpers.tpl | 62 - .../tailscale-proxy/templates/deployment.yaml | 156 --- charts/tailscale-proxy/templates/service.yaml | 22 - .../templates/serviceaccount.yaml | 17 - charts/tailscale-proxy/values.yaml | 84 -- cmd/greenhouse/controllers.go | 32 +- cmd/greenhouse/main.go | 12 - cmd/headscalectl/main.go | 12 - cmd/tailscale-starter/main.go | 10 - cmd/tcp-proxy/main.go | 114 -- docs/contribute/local-dev.md | 2 - docs/reference/api/index.html | 215 ---- docs/reference/api/openapi.yaml | 1062 ++++++++--------- docs/user-guides/cluster/onboarding.md | 4 +- go.mod | 31 +- go.sum | 70 -- pkg/apis/greenhouse/v1alpha1/cluster_types.go | 35 +- .../greenhouse/v1alpha1/conditions_test.go | 24 +- .../v1alpha1/zz_generated.deepcopy.go | 54 - pkg/apis/well_known.go | 8 +- pkg/clientutil/client.go | 15 - pkg/clientutil/headscale.go | 92 -- pkg/cmd/cluster_bootstrap.go | 377 +----- .../cluster/bootstrap_controller.go | 9 - .../cluster/bootstrap_controller_test.go | 2 +- .../cluster/cluster_exports_test.go | 17 +- .../cluster/headscale_access_controller.go | 505 -------- .../headscale_access_controller_test.go | 193 --- pkg/controllers/cluster/status_controller.go | 11 +- pkg/controllers/cluster/suite_test.go | 222 +--- pkg/controllers/cluster/util.go | 101 -- pkg/controllers/fixtures/cluster.yaml | 48 - pkg/controllers/fixtures/cluster_update.yaml | 48 - pkg/headscalectl/apikey.go | 256 ---- pkg/headscalectl/preauthkey.go | 261 ---- pkg/headscalectl/root.go | 52 - pkg/headscalectl/user.go | 254 ---- pkg/headscalectl/util.go | 132 -- pkg/tailscale-starter/root.go | 90 -- pkg/tailscale-starter/util.go | 105 -- pkg/tcp-proxy/metrics/metrics.go | 118 -- pkg/tcp-proxy/proxy/proxy.go | 167 --- pkg/test/headscale.go | 236 ---- test/e2e/controllers.go | 1 - types/typescript/schema.d.ts | 955 ++++++++------- 79 files changed, 1012 insertions(+), 6359 deletions(-) delete mode 100644 Dockerfile.headscalectl delete mode 100644 Dockerfile.tailscale delete mode 100644 Dockerfile.tcp-proxy delete mode 100644 charts/headscale/.helmignore delete mode 100644 charts/headscale/Chart.yaml delete mode 100644 charts/headscale/ci/test-values.yaml delete mode 100644 charts/headscale/templates/_helpers.tpl delete mode 100644 charts/headscale/templates/deployment.yaml delete mode 100644 charts/headscale/templates/etc/_acl_policy.yaml delete mode 100644 charts/headscale/templates/etc/_config.yaml delete mode 100644 charts/headscale/templates/etc/_derp.yaml delete mode 100644 charts/headscale/templates/etc/configmap.yaml delete mode 100644 charts/headscale/templates/ingress-grpc.yaml delete mode 100644 charts/headscale/templates/ingress.yaml delete mode 100644 charts/headscale/templates/postgres-service.yaml delete mode 100644 charts/headscale/templates/postgres-statefulset.yaml delete mode 100644 charts/headscale/templates/pvc.yaml delete mode 100644 charts/headscale/templates/rbac.yaml delete mode 100644 charts/headscale/templates/secret.yaml delete mode 100644 charts/headscale/templates/service.yaml delete mode 100644 charts/headscale/templates/serviceaccount.yaml delete mode 100644 charts/headscale/values.yaml delete mode 100644 charts/tailscale-proxy/.helmignore delete mode 100644 charts/tailscale-proxy/Chart.yaml delete mode 100644 charts/tailscale-proxy/templates/_helpers.tpl delete mode 100644 charts/tailscale-proxy/templates/deployment.yaml delete mode 100644 charts/tailscale-proxy/templates/service.yaml delete mode 100644 charts/tailscale-proxy/templates/serviceaccount.yaml delete mode 100644 charts/tailscale-proxy/values.yaml delete mode 100644 cmd/headscalectl/main.go delete mode 100644 cmd/tailscale-starter/main.go delete mode 100644 cmd/tcp-proxy/main.go delete mode 100644 pkg/clientutil/headscale.go delete mode 100644 pkg/controllers/cluster/headscale_access_controller.go delete mode 100644 pkg/controllers/cluster/headscale_access_controller_test.go delete mode 100644 pkg/headscalectl/apikey.go delete mode 100644 pkg/headscalectl/preauthkey.go delete mode 100644 pkg/headscalectl/root.go delete mode 100644 pkg/headscalectl/user.go delete mode 100644 pkg/headscalectl/util.go delete mode 100644 pkg/tailscale-starter/root.go delete mode 100644 pkg/tailscale-starter/util.go delete mode 100644 pkg/tcp-proxy/metrics/metrics.go delete mode 100644 pkg/tcp-proxy/proxy/proxy.go delete mode 100644 pkg/test/headscale.go diff --git a/.github/labeler.yml b/.github/labeler.yml index 74a77dd4f..2e070c99b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -15,13 +15,6 @@ documentation: - README.md - docs/** -headscale: -- changed-files: - - any-glob-to-any-file: - - charts/headscale/** - - pkg/headscale/** - - pkg/headscalectl/** - idproxy: - changed-files: - any-glob-to-any-file: diff --git a/.github/licenserc.yaml b/.github/licenserc.yaml index 63ec964bc..cf5906d08 100644 --- a/.github/licenserc.yaml +++ b/.github/licenserc.yaml @@ -37,8 +37,6 @@ header: - 'Makefile' - 'pkg/idproxy/web/**' - 'pkg/apis/scheme_builder.go' # Belongs to the Kubernetes authors - - 'cmd/tcp-proxy/main.go' # MIT License - - 'pkg/tcp-proxy/proxy/*.go' # MIT License - '**/zz_generated.deepcopy.go' # Generated by Kubebuilder - 'charts/**/templates/*.yaml' # license headers on helm templates are causing issues diff --git a/.github/workflows/docker-build.yaml b/.github/workflows/docker-build.yaml index cc0f5465f..cf01bbf7a 100644 --- a/.github/workflows/docker-build.yaml +++ b/.github/workflows/docker-build.yaml @@ -33,12 +33,6 @@ jobs: Imagename: greenhouse - Dockerfiles: Dockerfile.dev-env Imagename: greenhouse-dev-env - - Dockerfiles: Dockerfile.headscalectl - Imagename: greenhouse-headscalectl - - Dockerfiles: Dockerfile.tailscale - Imagename: greenhouse-tailscale - - Dockerfiles: Dockerfile.tcp-proxy - Imagename: greenhouse-tcp-proxy # - Dockerfiles: Dockerfile.dev-ui # Imagename: greenhouse-dev-ui @@ -151,12 +145,6 @@ jobs: Imagename: greenhouse - Dockerfiles: Dockerfile.dev-env Imagename: greenhouse-dev-env - - Dockerfiles: Dockerfile.headscalectl - Imagename: greenhouse-headscalectl - - Dockerfiles: Dockerfile.tailscale - Imagename: greenhouse-tailscale - - Dockerfiles: Dockerfile.tcp-proxy - Imagename: greenhouse-tcp-proxy # - Dockerfiles: Dockerfile.dev-ui # Imagename: greenhouse-dev-ui diff --git a/.github/workflows/pr-docker-build.yaml b/.github/workflows/pr-docker-build.yaml index 5e8ce6878..deace2165 100644 --- a/.github/workflows/pr-docker-build.yaml +++ b/.github/workflows/pr-docker-build.yaml @@ -25,12 +25,6 @@ jobs: Imagename: greenhouse - Dockerfiles: Dockerfile.dev-env Imagename: greenhouse-dev-env - - Dockerfiles: Dockerfile.headscalectl - Imagename: greenhouse-headscalectl - - Dockerfiles: Dockerfile.tailscale - Imagename: greenhouse-tailscale - - Dockerfiles: Dockerfile.tcp-proxy - Imagename: greenhouse-tcp-proxy permissions: contents: read diff --git a/Dockerfile.headscalectl b/Dockerfile.headscalectl deleted file mode 100644 index 472ad8b92..000000000 --- a/Dockerfile.headscalectl +++ /dev/null @@ -1,26 +0,0 @@ -# Build the manager binary -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.23 AS builder - -ARG TARGETOS -ARG TARGETARCH -ENV CGO_ENABLED=0 - -WORKDIR /workspace - -COPY Makefile . -COPY . . - -# Build greenhouse operator and tooling. -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - make build-headscalectl CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} - -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM --platform=${BUILDPLATFORM:-linux/amd64} gcr.io/distroless/static:latest -LABEL source_repository="https://github.com/cloudoperators/greenhouse" -WORKDIR / -COPY --from=builder /workspace/bin/* . - -RUN ["/headscalectl", "--version"] -ENTRYPOINT ["/headscalectl"] diff --git a/Dockerfile.tailscale b/Dockerfile.tailscale deleted file mode 100644 index ac4246118..000000000 --- a/Dockerfile.tailscale +++ /dev/null @@ -1,30 +0,0 @@ -# Build the manager binary -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.23 AS builder - -ARG TARGETOS -ARG TARGETARCH -ENV CGO_ENABLED=0 - -WORKDIR /workspace - -COPY Makefile . -COPY . . - -# Build greenhouse operator and tooling. -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - make build-tailscale-starter CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} - -FROM --platform=${BUILDPLATFORM:-linux/amd64} ghcr.io/tailscale/tailscale:v1.74.1 -LABEL source_repository="https://github.com/cloudoperators/greenhouse" - -# upgrade all installed packages to fix potential CVEs in advance -RUN apk upgrade --no-cache --no-progress \ - && apk del --no-cache --no-progress apk-tools alpine-keys - -COPY --from=builder /workspace/bin/* . - -RUN mkdir /tailscale && ln -s /usr/local/bin/containerboot /tailscale/run.sh - -RUN ["/tailscale-starter", "--version"] -ENTRYPOINT ["/tailscale-starter"] diff --git a/Dockerfile.tcp-proxy b/Dockerfile.tcp-proxy deleted file mode 100644 index 00f92f4c4..000000000 --- a/Dockerfile.tcp-proxy +++ /dev/null @@ -1,27 +0,0 @@ -# Build the manager binary -FROM --platform=${BUILDPLATFORM:-linux/amd64} golang:1.23 AS builder - -ARG TARGETOS -ARG TARGETARCH -ENV CGO_ENABLED=0 - -WORKDIR /workspace - -COPY Makefile . -COPY . . - -# Build greenhouse operator and tooling. -RUN --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - make build-tcp-proxy CGO_ENABLED=${CGO_ENABLED} GOOS=${TARGETOS} GOARCH=${TARGETARCH} - -# Use distroless as minimal base image to package the manager binary -# Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM --platform=${BUILDPLATFORM:-linux/amd64} gcr.io/distroless/static:nonroot -LABEL source_repository="https://github.com/cloudoperators/greenhouse" -WORKDIR / -COPY --from=builder /workspace/bin/* . -USER 65532:65532 - -RUN ["/tcp-proxy", "--version"] -ENTRYPOINT ["/tcp-proxy"] diff --git a/charts/greenhouse/Chart.lock b/charts/greenhouse/Chart.lock index c4cb66860..f2515598e 100644 --- a/charts/greenhouse/Chart.lock +++ b/charts/greenhouse/Chart.lock @@ -5,12 +5,6 @@ dependencies: - name: cors-proxy repository: file://../cors-proxy version: 0.2.0 -- name: headscale - repository: file://../headscale - version: 0.1.3 -- name: tailscale-proxy - repository: file://../tailscale-proxy - version: 0.1.0 - name: manager repository: file://../manager version: 0.1.6 @@ -20,5 +14,5 @@ dependencies: - name: demo repository: file://../demo version: 0.1.1 -digest: sha256:a9f3691f99496f2a587ab98e599c7b0cc00c0c5a6e712b75c2fc070aedd1615b -generated: "2024-09-06T14:19:08.271429486+02:00" +digest: sha256:fd38eac090116758caa914c00b9fb1065f08ccda7685f858007a3939ff6efbb4 +generated: "2024-10-09T11:57:04.231793+02:00" diff --git a/charts/greenhouse/Chart.yaml b/charts/greenhouse/Chart.yaml index 946344f87..b3ee28a38 100644 --- a/charts/greenhouse/Chart.yaml +++ b/charts/greenhouse/Chart.yaml @@ -5,7 +5,7 @@ apiVersion: v2 name: greenhouse description: A Helm chart for deploying greenhouse type: application -version: 0.2.8 +version: 0.3.0 appVersion: "0.1.0" dependencies: @@ -17,14 +17,6 @@ dependencies: name: cors-proxy repository: "file://../cors-proxy" version: 0.2.0 - - condition: headscale.enabled - name: headscale - version: 0.1.3 - repository: "file://../headscale" - - condition: tailscale-proxy.enabled - name: tailscale-proxy - version: 0.1.0 - repository: "file://../tailscale-proxy" - name: manager version: 0.1.6 repository: "file://../manager" diff --git a/charts/greenhouse/ci/test-values.yaml b/charts/greenhouse/ci/test-values.yaml index a738a2762..a949d3054 100644 --- a/charts/greenhouse/ci/test-values.yaml +++ b/charts/greenhouse/ci/test-values.yaml @@ -39,12 +39,6 @@ alerts: host: topSecret! endpoint: topSecret! -headscale: - ingress: - host: "foo.bar" - postgres: - password: topSecret! - tailscale-proxy: preauthkeyProvosioner: userName: topSecret! diff --git a/charts/headscale/.helmignore b/charts/headscale/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/charts/headscale/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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/charts/headscale/Chart.yaml b/charts/headscale/Chart.yaml deleted file mode 100644 index 3ae8ba184..000000000 --- a/charts/headscale/Chart.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -# SPDX-License-Identifier: Apache-2.0 - -apiVersion: v2 -name: headscale -description: Headscale -type: application -version: 0.1.3 -appVersion: 0.22.3 diff --git a/charts/headscale/ci/test-values.yaml b/charts/headscale/ci/test-values.yaml deleted file mode 100644 index fe370158a..000000000 --- a/charts/headscale/ci/test-values.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -# SPDX-License-Identifier: Apache-2.0 - -ingress: - enabled: true - host: headscale.example.com - -grpc: - insecure: true - ingress: - enabled: true - host: headscale-grpc.example.com - -postgres: - password: topSecret! - -oidc: - enabled: true - issuer: https://auth.greenhouse.com - clientID: topSecret! - clientSecret: topSecret! - allowedGroups: - - role:greenhouse:admin diff --git a/charts/headscale/templates/_helpers.tpl b/charts/headscale/templates/_helpers.tpl deleted file mode 100644 index d623670aa..000000000 --- a/charts/headscale/templates/_helpers.tpl +++ /dev/null @@ -1,84 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "headscale.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 "headscale.fullname" -}} -{{- $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 }} - - -{{/* -Create chart name and version as used by the chart label. -*/}} -{{- define "headscale.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "common.labels" -}} -helm.sh/chart: {{ include "headscale.chart" . }} -{{ include "common.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Headscale labels -*/}} -{{- define "headscale.labels" -}} -{{- include "common.labels" .}} -app.kubernetes.io/component: headscale -{{- end -}} - -{{/* -Common selector labels -*/}} -{{- define "common.selectorLabels" -}} -app.kubernetes.io/name: {{ include "headscale.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Headscale selector labels -*/}} -{{- define "headscale.selectorLabels" -}} -{{- include "common.selectorLabels" .}} -app.kubernetes.io/component: headscale -{{- end -}} - -{{/* -Postgres labels -*/}} -{{- define "postgres.labels"}} -{{- include "common.labels" . }} -app.kubernetes.io/component: postgres -{{- end }} - -{{- define "postgres.fullname" -}} -{{- include "headscale.fullname" . }}-postgres -{{- end }} - -{{/* -Postgres selector labels -*/}} -{{- define "postgres.selectorLabels" -}} -{{- include "common.selectorLabels" . }} -app.kubernetes.io/component: postgres -{{- end }} diff --git a/charts/headscale/templates/deployment.yaml b/charts/headscale/templates/deployment.yaml deleted file mode 100644 index 1d2485d95..000000000 --- a/charts/headscale/templates/deployment.yaml +++ /dev/null @@ -1,122 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{- $fullName := include "headscale.fullname" . -}} -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "headscale.fullname" . }} - labels: - {{- include "headscale.labels" . | nindent 4 }} -spec: - replicas: 1 - strategy: - type: Recreate - selector: - matchLabels: - {{- include "headscale.selectorLabels" . | nindent 6 }} - template: - metadata: - annotations: - checksum/configmap: {{ include (print $.Template.BasePath "/etc/configmap.yaml") . | sha256sum }} - checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} - labels: - {{- include "headscale.labels" . | nindent 8 }} - spec: - serviceAccountName: {{ include "headscale.fullname" . }} - initContainers: - - name: wait-for-postgres - image: "{{ required ".Values.postgres.image.repository missing" .Values.postgres.image.repository }}:{{ required ".Values.postgres.image.tag missing" .Values.postgres.image.tag }}" - command: - - sh - - -c - - "until pg_isready; do echo waiting for database; sleep 2; done;" - env: - - name: PGHOST - value: "{{ include "postgres.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local" - - name: PGPORT - value: {{ required ".Values.postgres.service.port missing" .Values.postgres.service.port | quote }} - - name: PGDATABASE - value: headscale - - name: PGUSER - value: {{ required ".Values.postgres.username missing" .Values.postgres.username }} - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: {{ include "headscale.fullname" . }} - key: POSTGRES_PASSWORD - containers: - - name: headscale - image: "{{ required ".Values.server.image.repository missing" .Values.server.image.repository }}:{{ .Chart.AppVersion }}" - imagePullPolicy: {{ required ".Values.server.image.pullPolicy missing" .Values.server.image.pullPolicy }} - command: ["headscale", "serve"] - env: - - name: DB_PASS - valueFrom: - secretKeyRef: - name: {{ include "headscale.fullname" . }} - key: POSTGRES_PASSWORD - ports: - {{- range $name, $v := .Values.server.service }} - - name: {{ $name }} - protocol: TCP - containerPort: {{ $v.port }} - {{- end }} - livenessProbe: - tcpSocket: - port: http - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 15 - volumeMounts: - - name: config - mountPath: /vol/config - - name: secret - mountPath: /vol/secret - - name: config - mountPath: /etc/headscale - - name: data - mountPath: /var/lib/headscale - - name: socket - mountPath: /var/run/headscale - - name: headscale-ui - image: "{{ required ".Values.ui.image.repository missing" .Values.ui.image.repository }}:{{ required ".Values.ui.image.tag missing" .Values.ui.image.tag }}" - imagePullPolicy: {{ required ".Values.ui.image.pullPolicy missing" .Values.ui.image.pullPolicy }} - ports: - - name: http - protocol: TCP - containerPort: {{ required ".Values.ui.service.port missing" .Values.ui.service.port }} - - name: headscalectl - image: "{{ required ".Values.headscalectl.image.repository missing" .Values.headscalectl.image.repository }}:{{ required ".Values.headscalectl.image.tag missing" .Values.headscalectl.image.tag }}" - imagePullPolicy: {{ required ".Values.headscalectl.image.pullPolicy missing" .Values.headscalectl.image.pullPolicy }} - args: - - apikey - - create - - --output - - secret - - --secret-name - - {{ required ".Values.headscalectl.secret.name missing" .Values.headscalectl.secret.name }} - - --secret-namespace - - {{ .Release.Namespace }} - - --socket - - --socket-path - - /tmp/headscale.sock - volumeMounts: - - name: socket - mountPath: /tmp/ - volumes: - - name: config - configMap: - name: {{ include "headscale.fullname" . }}-config - - name: secret - secret: - secretName: {{ include "headscale.fullname" . }} - - name: data - persistentVolumeClaim: - claimName: {{ include "headscale.fullname" . }} - - name: socket - emptyDir: - medium: Memory - sizeLimit: 10Mi diff --git a/charts/headscale/templates/etc/_acl_policy.yaml b/charts/headscale/templates/etc/_acl_policy.yaml deleted file mode 100644 index 41b0847eb..000000000 --- a/charts/headscale/templates/etc/_acl_policy.yaml +++ /dev/null @@ -1,45 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{ - "Groups":{ - "group:admin":[ - "manager" - ] - }, - "TagOwners":{ - "tag:greenhouse":[ - "group:admin" - ], - "tag:client":[ - "group:admin" - ] - }, - "Hosts":{ - "client-subnet":{{ required ".Values.clientSubnet missing" .Values.clientSubnet | quote }} - }, - "ACLs":[ - { - "action":"accept", - "src":[ - "group:admin" - ], - "dst":[ - "*:{{ required ".Values.server.service.http.port missing" .Values.server.service.http.port }}", - "*:53", - "*:443" - ] - }, - { - "action":"accept", - "src":[ - "*" - ], - "dst":[ - "*:53", - ] - } - ] -} diff --git a/charts/headscale/templates/etc/_config.yaml b/charts/headscale/templates/etc/_config.yaml deleted file mode 100644 index 2d216f3aa..000000000 --- a/charts/headscale/templates/etc/_config.yaml +++ /dev/null @@ -1,129 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -server_url: http://{{ required ".Values.ingress.host missing" .Values.ingress.host }} -listen_addr: "0.0.0.0:{{ required ".Values.server.service.http.port missing" .Values.server.service.http.port }}" -metrics_listen_addr: "127.0.0.1:{{ required ".Values.server.service.metrics.port missing" .Values.server.service.metrics.port }}" -ephemeral_node_inactivity_timeout: "30m" -acl_policy_path: /etc/headscale/acl.policy -private_key_path: /var/lib/headscale/private.key -noise: - private_key_path: /var/lib/headscale/noise_private.key - -grpc_listen_addr: "0.0.0.0:{{ required ".Values.server.service.grpc.port missing" .Values.server.service.grpc.port }}" -grpc_allow_insecure: {{ .Values.grpc.insecure }} - -dns_config: - base_domain: {{ required ".Values.ingress.host missing" .Values.ingress.host }} - magic_dns: false - override_local_dns: true - {{- if and .Values.dnsConfig .Values.dnsConfig.nameservers }} - nameservers: - {{- toYaml .Values.dnsConfig.nameservers | nindent 4 }} - {{ end }} - {{- if and .Values.dnsConfig .Values.dnsConfig.restrictedNameservers }} - restricted_nameservers: - {{- toYaml .Values.dnsConfig.restrictedNameservers | nindent 4 }} - {{ end }} - - # Search domains to inject. - domains: [sap,sap.corp,svc,svc.cluster.local,cluster.local] - -# headscale needs a list of DERP servers that can be presented -# to the clients. -derp: - server: - # If enabled, runs the embedded DERP server and merges it into the rest of the DERP config - # The Headscale server_url defined above MUST be using https, DERP requires TLS to be in place - enabled: false - - # Region ID to use for the embedded DERP server. - # The local DERP prevails if the region ID collides with other region ID coming from - # the regular DERP config. - region_id: 999 - - # Region code and name are displayed in the Tailscale UI to identify a DERP region - region_code: "headscale" - region_name: "Headscale Embedded DERP" - - # Listens over UDP at the configured address for STUN connections - to help with NAT traversal. - # When the embedded DERP server is enabled stun_listen_addr MUST be defined. - # - # For more details on how this works, check this great article: https://tailscale.com/blog/how-tailscale-works/ - stun_listen_addr: "0.0.0.0:3478" - - # List of externally available DERP maps encoded in JSON - # urls: - # - https://controlplane.tailscale.com/derpmap/default - - # Locally available DERP map files encoded in YAML - # - # This option is mostly interesting for people hosting - # their own DERP servers: - # https://tailscale.com/kb/1118/custom-derp-servers/ - # - # paths: - # - /etc/headscale/derp-example.yaml - paths: - - /etc/headscale/derp.yaml - - # If enabled, a worker will be set up to periodically - # refresh the given sources and update the derpmap - # will be set up. - auto_update_enabled: true - - # How often should we check for DERP updates? - update_frequency: 24h - -# List of IP prefixes to allocate tailaddresses from. -# Each prefix consists of either an IPv4 or IPv6 address, -# and the associated prefix length, delimited by a slash. -# It must be within IP ranges supported by the Tailscale -# client - i.e., subnets of 100.64.0.0/10 and fd7a:115c:a1e0::/48. -# See below: -# IPv6: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#LL81C52-L81C71 -# IPv4: https://github.com/tailscale/tailscale/blob/22ebb25e833264f58d7c3f534a8b166894a89536/net/tsaddr/tsaddr.go#L33 -# Any other range is NOT supported, and it will cause unexpected issues. -ip_prefixes: - - {{ required ".Values.clientSubnet missing" .Values.clientSubnet | quote }} -# Period to check for node updates within the tailnet. A value too low will severely affect -# CPU consumption of Headscale. A value too high (over 60s) will cause problems -# for the nodes, as they won't get updates or keep alive messages frequently enough. -# In case of doubts, do not touch the default 10s. -node_update_check_interval: 10s - -## Use already defined certificates: -#tls_cert_path: /certs/tls.crt -#tls_key_path: /certs/tls.key - -log: - # Output formatting for logs: text or json - format: text - level: info - -# Enabling this option makes devices prefer a random port for WireGuard traffic over the -# default static port 41641. This option is intended as a workaround for some buggy -# firewall devices. See https://tailscale.com/kb/1181/firewalls/ for more information. -randomize_client_port: false - -# DB values. -db_type: postgres -db_name: headscale -db_host: "{{ include "postgres.fullname" . }}.{{ .Release.Namespace }}.svc.cluster.local" -db_port: {{ required ".Values.postgres.service.port missing" .Values.postgres.service.port }} -db_user: {{ required ".Values.postgres.username missing" .Values.postgres.username }} -# FIXME: This should be injected via DB_PASS environment variable. -db_pass: {{ required ".Values.postgres.password missing" .Values.postgres.password }} - -{{ if .Values.oidc.enabled -}} -oidc: - issuer: {{ required ".Values.oidc.issuer missing" .Values.oidc.issuer }} - client_id: {{ required ".Values.oidc.clientID missing" .Values.oidc.clientID }} - client_secret_path: /vol/secret/oidc_client_secret - {{ if .Values.oidc.allowedGroups -}} - allowed_groups: - {{- toYaml .Values.oidc.allowedGroups | nindent 4 -}} - {{ end }} -{{ end }} diff --git a/charts/headscale/templates/etc/_derp.yaml b/charts/headscale/templates/etc/_derp.yaml deleted file mode 100644 index 6cc807d11..000000000 --- a/charts/headscale/templates/etc/_derp.yaml +++ /dev/null @@ -1,8 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{- if .Values.derp.enabled }} -{{ toYaml .Values.derp}} -{{ end }} diff --git a/charts/headscale/templates/etc/configmap.yaml b/charts/headscale/templates/etc/configmap.yaml deleted file mode 100644 index d7c2feb06..000000000 --- a/charts/headscale/templates/etc/configmap.yaml +++ /dev/null @@ -1,23 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: ConfigMap -metadata: - name: {{ include "headscale.fullname" . }}-config - labels: - {{- include "headscale.labels" . | nindent 4 }} - -data: - config.yaml: | - {{ include (print $.Template.BasePath "/etc/_config.yaml") . | nindent 4 }} - - acl.policy: | - {{ include (print $.Template.BasePath "/etc/_acl_policy.yaml") . | nindent 4 }} - -{{- if .Values.derp.enabled }} - derp.yaml: | - {{ include (print $.Template.BasePath "/etc/_derp.yaml") . | nindent 4 }} -{{ end }} diff --git a/charts/headscale/templates/ingress-grpc.yaml b/charts/headscale/templates/ingress-grpc.yaml deleted file mode 100644 index 87c5031cd..000000000 --- a/charts/headscale/templates/ingress-grpc.yaml +++ /dev/null @@ -1,35 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{ if .Values.grpc.ingress.enabled }} -{{- $fullName := include "headscale.fullname" . -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ $fullName }}-grpc - labels: - {{- include "headscale.labels" . | nindent 4 }} - {{- with .Values.grpc.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - ingressClassName: {{ required ".Values.grpc.ingress.className missing" .Values.grpc.ingress.className }} - tls: - - hosts: - - {{ .Values.grpc.ingress.host }} - secretName: "tls-{{ $fullName }}-grpc" - rules: - - host: {{ .Values.grpc.ingress.host }} - http: - paths: - - path: / - pathType: ImplementationSpecific - backend: - service: - name: {{ $fullName }} - port: - name: grpc -{{- end }} diff --git a/charts/headscale/templates/ingress.yaml b/charts/headscale/templates/ingress.yaml deleted file mode 100644 index 4b15c7082..000000000 --- a/charts/headscale/templates/ingress.yaml +++ /dev/null @@ -1,46 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{ if .Values.ingress.enabled }} -{{- $fullName := include "headscale.fullname" . -}} -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: {{ $fullName }} - labels: - {{- include "headscale.labels" . | nindent 4 }} - {{- with .Values.ingress.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -spec: - ingressClassName: {{ required ".Values.ingress.className missing" .Values.ingress.className }} - tls: - - hosts: - - {{ .Values.ingress.host }} - secretName: "tls-{{ $fullName }}" - rules: - - host: {{ .Values.ingress.host }} - http: - paths: - {{- range .Values.server.service }} - {{- if .path }} - - path: {{ .path }} - pathType: ImplementationSpecific - backend: - service: - name: {{ $fullName }} - port: - number: {{ .port }} - {{- end }} - {{- end }} - - path: {{ .Values.ui.service.path }} - pathType: ImplementationSpecific - backend: - service: - name: {{ $fullName }} - port: - number: {{ .Values.ui.service.port }} -{{- end }} diff --git a/charts/headscale/templates/postgres-service.yaml b/charts/headscale/templates/postgres-service.yaml deleted file mode 100644 index f049839d3..000000000 --- a/charts/headscale/templates/postgres-service.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: Service -metadata: - name: {{ include "postgres.fullname" . }} - labels: - {{- include "postgres.labels" . | nindent 4 }} -spec: - selector: - {{- include "postgres.selectorLabels" . | nindent 4 }} - ports: - - name: postgres - targetPort: postgres - port: {{ required ".Values.postgres.service.port missing" .Values.postgres.service.port }} diff --git a/charts/headscale/templates/postgres-statefulset.yaml b/charts/headscale/templates/postgres-statefulset.yaml deleted file mode 100644 index c0d31b064..000000000 --- a/charts/headscale/templates/postgres-statefulset.yaml +++ /dev/null @@ -1,58 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: apps/v1 -kind: StatefulSet -metadata: - name: {{ include "postgres.fullname" . }} - labels: - {{- include "postgres.labels" . | nindent 4 }} -spec: - serviceName: {{ include "postgres.fullname" . }} - replicas: 1 - selector: - matchLabels: - {{- include "postgres.selectorLabels" . | nindent 6 }} - template: - metadata: - labels: - {{- include "postgres.labels" . | nindent 8 }} - spec: - containers: - - name: postgres - image: "{{ required ".Values.postgres.image.repository missing" .Values.postgres.image.repository }}:{{ required ".Values.postgres.image.tag missing" .Values.postgres.image.tag }}" - imagePullPolicy: {{ required ".Values.postgres.image.pullPolicy missing" .Values.postgres.image.pullPolicy }} - env: - - name: PGDATA - value: /headscale/postgresql/data/pgdata - - name: POSTGRES_PASSWORD - valueFrom: - secretKeyRef: - name: {{ include "headscale.fullname" . }} - key: POSTGRES_PASSWORD - - name: POSTGRES_USER - value: {{ required ".Values.postgres.username missing" .Values.postgres.username }} - ports: - - name: postgres - protocol: TCP - containerPort: 5432 - livenessProbe: - tcpSocket: - port: 5432 - initialDelaySeconds: 30 - timeoutSeconds: 5 - periodSeconds: 15 - volumeMounts: - - name: {{ include "postgres.fullname" . }} - mountPath: /headscale/postgresql - subPath: data - volumeClaimTemplates: - - metadata: - name: {{ include "postgres.fullname" . }} - spec: - accessModes: ["ReadWriteOnce"] - resources: - requests: - storage: {{ required ".Values.postgres.pvc.storage missing" .Values.postgres.pvc.storage }} diff --git a/charts/headscale/templates/pvc.yaml b/charts/headscale/templates/pvc.yaml deleted file mode 100644 index 2978d497f..000000000 --- a/charts/headscale/templates/pvc.yaml +++ /dev/null @@ -1,18 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: {{ include "headscale.fullname" . }} - labels: - {{- include "headscale.labels" . | nindent 4 }} -spec: - storageClassName: default - accessModes: - - ReadWriteMany - resources: - requests: - storage: {{ required ".Values.server.pvc.storage missing" .Values.server.pvc.storage }} diff --git a/charts/headscale/templates/rbac.yaml b/charts/headscale/templates/rbac.yaml deleted file mode 100644 index a3045ded0..000000000 --- a/charts/headscale/templates/rbac.yaml +++ /dev/null @@ -1,34 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: rbac.authorization.k8s.io/v1 -kind: Role -metadata: - name: {{ include "headscale.fullname" . }}-role -rules: -- apiGroups: - - "" - resources: - - secrets - verbs: - - get - - patch - - create - - update ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: RoleBinding -metadata: - name: {{ include "headscale.fullname" . }}-rbac - labels: - {{- include "headscale.labels" . | nindent 4 }} -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: Role - name: '{{ include "headscale.fullname" . }}-role' -subjects: -- kind: ServiceAccount - name: {{ include "headscale.fullname" . }} - namespace: {{ .Release.Namespace }} diff --git a/charts/headscale/templates/secret.yaml b/charts/headscale/templates/secret.yaml deleted file mode 100644 index cfd0cdabb..000000000 --- a/charts/headscale/templates/secret.yaml +++ /dev/null @@ -1,16 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: Secret -metadata: - name: {{ include "headscale.fullname" . }} - labels: - {{- include "headscale.labels" . | nindent 4 }} -data: - POSTGRES_PASSWORD: {{ required ".Values.postgres.password missing" .Values.postgres.password | b64enc }} - {{ if .Values.oidc.enabled }} - oidc_client_secret: {{ required ".Values.oidc.clientSecret missing" .Values.oidc.clientSecret | b64enc }} - {{ end }} diff --git a/charts/headscale/templates/service.yaml b/charts/headscale/templates/service.yaml deleted file mode 100644 index 544ca7a10..000000000 --- a/charts/headscale/templates/service.yaml +++ /dev/null @@ -1,26 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: Service -metadata: - name: {{ include "headscale.fullname" . }} - labels: - {{- include "headscale.labels" . | nindent 4 }} -spec: - type: ClusterIP - ports: - {{- range $name, $v := .Values.server.service }} - - name: {{ $name }} - port: {{ $v.port }} - targetPort: {{ $v.port }} - protocol: TCP - {{- end }} - - name: ui - port: {{ required ".Values.ui.service.port missing" .Values.ui.service.port }} - targetPort: {{ required ".Values.ui.service.port missing" .Values.ui.service.port }} - protocol: TCP - selector: - {{- include "headscale.selectorLabels" . | nindent 4 }} diff --git a/charts/headscale/templates/serviceaccount.yaml b/charts/headscale/templates/serviceaccount.yaml deleted file mode 100644 index aa93bd1c9..000000000 --- a/charts/headscale/templates/serviceaccount.yaml +++ /dev/null @@ -1,11 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "headscale.fullname" . }} - labels: - {{- include "headscale.labels" . | nindent 4 }} diff --git a/charts/headscale/values.yaml b/charts/headscale/values.yaml deleted file mode 100644 index a9174f361..000000000 --- a/charts/headscale/values.yaml +++ /dev/null @@ -1,98 +0,0 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -# SPDX-License-Identifier: Apache-2.0 - -# Headscale coordination server. -server: - image: - repository: juanfont/headscale - pullPolicy: IfNotPresent - - # Services with a path will be included in the ingress routes. - service: - http: - port: 8080 - path: / - grpc: - port: 50443 - derp: - port: 3478 - metrics: - port: 9090 - - pvc: - storage: 1Gi - -ui: - image: - repository: ghcr.io/gurucomputing/headscale-ui - tag: 2023.01.30-beta-1 - pullPolicy: IfNotPresent - service: - port: 80 - path: /web - -headscalectl: - image: - repository: ghcr.io/cloudoperators/greenhouse-headscalectl - tag: main - pullPolicy: Always - secret: - name: tailscale-auth - -ingress: - enabled: true - host: - className: nginx - annotations: - disco: "true" - kubernetes.io/tls-acme: "true" - -grpc: - insecure: false - ingress: - enabled: false - host: - className: nginx - annotations: - disco: "true" - kubernetes.io/tls-acme: "true" - nginx.ingress.kubernetes.io/backend-protocol: "GRPC" - nginx.ingress.kubernetes.io/ssl-redirect: "true" - -postgres: - username: headscale - # Mandatory password to be supplied via secrets. - password: - - service: - port: 5432 - - image: - repository: postgres - tag: 16.4 - pullPolicy: IfNotPresent - - pvc: - storage: 1Gi - -oidc: - enabled: false - issuer: - clientID: - clientSecret: - allowedGroups: - - role:greenhouse:admin - -derp: - enabled: false - -# IP prefixes to allocate tailaddresses from. -clientSubnet: 1.2.3.4/5 - -# Headscale DNS configuration. -dnsConfig: - nameservers: [] - # - 1.1.1.1 - restrictedNameservers: {} - # nameserver1: - # - 1.1.1.1 diff --git a/charts/manager/crds/greenhouse.sap_clusters.yaml b/charts/manager/crds/greenhouse.sap_clusters.yaml index fb851295b..c25b8c50e 100644 --- a/charts/manager/crds/greenhouse.sap_clusters.yaml +++ b/charts/manager/crds/greenhouse.sap_clusters.yaml @@ -57,7 +57,6 @@ spec: the Greenhouse operator. enum: - direct - - headscale type: string required: - accessMode @@ -70,53 +69,6 @@ spec: timestamp of the bearer token used to access the cluster. format: date-time type: string - headScaleStatus: - description: HeadScaleStatus contains the current status of the headscale - client. - properties: - createdAt: - format: date-time - type: string - expiry: - format: date-time - type: string - forcedTags: - items: - type: string - type: array - id: - format: int64 - type: integer - ipAddresses: - items: - type: string - type: array - name: - type: string - online: - type: boolean - preAuthKey: - description: PreAuthKey reflects the status of the pre-authentication - key used by the Headscale machine. - properties: - createdAt: - format: date-time - type: string - ephemeral: - type: boolean - expiration: - format: date-time - type: string - id: - type: string - reusable: - type: boolean - used: - type: boolean - user: - type: string - type: object - type: object kubernetesVersion: description: KubernetesVersion reflects the detected Kubernetes version of the cluster. diff --git a/charts/manager/templates/deployment.yaml b/charts/manager/templates/deployment.yaml index cbf74b4eb..cec2d1274 100644 --- a/charts/manager/templates/deployment.yaml +++ b/charts/manager/templates/deployment.yaml @@ -52,17 +52,6 @@ spec: containerName: manager divisor: "0" resource: limits.cpu - {{- if .Values.headscale.enabled }} - - name: HEADSCALE_API_URL - value: {{ required ".Values.headscale.apiURL missing" .Values.headscale.apiURL }} - - name: HEADSCALE_API_KEY - valueFrom: - secretKeyRef: - name: {{ required ".Values.headscale.apiKeySecret missing" .Values.headscale.apiKeySecret }} - key: HEADSCALE_CLI_API_KEY - - name: TAILSCALE_PROXY - value: {{ required ".Values.headscale.proxyURL missing" .Values.headscale.proxyURL }} - {{ end }} image: {{ .Values.controllerManager.image.repository }}:{{ .Values.controllerManager.image.tag | default .Chart.AppVersion }} livenessProbe: httpGet: diff --git a/charts/manager/values.yaml b/charts/manager/values.yaml index 68e9a6e6c..c085f4ab4 100644 --- a/charts/manager/values.yaml +++ b/charts/manager/values.yaml @@ -6,13 +6,6 @@ nameOverride: greenhouse alerts: enabled: true -# TODO: Move to globals to share with headscale chart -headscale: - enabled: false - apiURL: - apiKeySecret: tailscale-auth - proxyURL: socks5://greenhouse-tailscale-proxy.greenhouse.svc.cluster.local:1055 - controllerManager: containerSecurityContext: allowPrivilegeEscalation: false diff --git a/charts/tailscale-proxy/.helmignore b/charts/tailscale-proxy/.helmignore deleted file mode 100644 index 0e8a0eb36..000000000 --- a/charts/tailscale-proxy/.helmignore +++ /dev/null @@ -1,23 +0,0 @@ -# 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/charts/tailscale-proxy/Chart.yaml b/charts/tailscale-proxy/Chart.yaml deleted file mode 100644 index 92eafe72a..000000000 --- a/charts/tailscale-proxy/Chart.yaml +++ /dev/null @@ -1,27 +0,0 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -# SPDX-License-Identifier: Apache-2.0 - -apiVersion: v2 -name: tailscale-proxy -description: A Helm chart to deploy tailscale with userspace networking which exposes a socks5 and an http proxy - -# A chart can be either an 'application' or a 'library' chart. -# -# Application charts are a collection of templates that can be packaged into versioned archives -# to be deployed. -# -# Library charts provide useful utilities or functions for the chart developer. They're included as -# a dependency of application charts to inject those utilities and functions into the rendering -# pipeline. Library charts do not define any templates and therefore cannot be deployed. -type: application - -# This is the chart version. This version number should be incremented each time you make changes -# to the chart and its templates, including the app version. -# Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.1.0 - -# This is the version number of the application being deployed. This version number should be -# incremented each time you make changes to the application. Versions are not expected to -# follow Semantic Versioning. They should reflect the version the application is using. -# It is recommended to use it with quotes. -appVersion: "1.50.1" diff --git a/charts/tailscale-proxy/templates/_helpers.tpl b/charts/tailscale-proxy/templates/_helpers.tpl deleted file mode 100644 index 52135ff15..000000000 --- a/charts/tailscale-proxy/templates/_helpers.tpl +++ /dev/null @@ -1,62 +0,0 @@ -{{/* -Expand the name of the chart. -*/}} -{{- define "tailscale-proxy.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 "tailscale-proxy.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 "tailscale-proxy.chart" -}} -{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} -{{- end }} - -{{/* -Common labels -*/}} -{{- define "tailscale-proxy.labels" -}} -helm.sh/chart: {{ include "tailscale-proxy.chart" . }} -{{ include "tailscale-proxy.selectorLabels" . }} -{{- if .Chart.AppVersion }} -app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} -{{- end }} -app.kubernetes.io/managed-by: {{ .Release.Service }} -{{- end }} - -{{/* -Selector labels -*/}} -{{- define "tailscale-proxy.selectorLabels" -}} -app.kubernetes.io/name: {{ include "tailscale-proxy.name" . }} -app.kubernetes.io/instance: {{ .Release.Name }} -{{- end }} - -{{/* -Create the name of the service account to use -*/}} -{{- define "tailscale-proxy.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "tailscale-proxy.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} -{{- end }} diff --git a/charts/tailscale-proxy/templates/deployment.yaml b/charts/tailscale-proxy/templates/deployment.yaml deleted file mode 100644 index 693ccd136..000000000 --- a/charts/tailscale-proxy/templates/deployment.yaml +++ /dev/null @@ -1,156 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: apps/v1 -kind: Deployment -metadata: - name: {{ include "tailscale-proxy.fullname" . }} - labels: - {{- include "tailscale-proxy.labels" . | nindent 4 }} -spec: - replicas: {{ .Values.replicaCount }} - selector: - matchLabels: - {{- include "tailscale-proxy.selectorLabels" . | nindent 6 }} - template: - metadata: - {{- with .Values.podAnnotations }} - annotations: - {{- toYaml . | nindent 8 }} - {{- end }} - labels: - {{- include "tailscale-proxy.selectorLabels" . | nindent 8 }} - spec: - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: app.kubernetes.io/name - operator: In - values: - - {{ include "tailscale-proxy.name" . }} - topologyKey: "kubernetes.io/hostname" - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - serviceAccountName: {{ include "tailscale-proxy.serviceAccountName" . }} - initContainers: - - name: preauthkey-provisioner - image: "{{ .Values.preauthkeyProvosioner.image.repository }}:{{ .Values.preauthkeyProvosioner.image.tag}}" - imagePullPolicy: {{ .Values.preauthkeyProvosioner.image.pullPolicy }} - args: - - "preauthkey" - - "create" - {{- with .Values.preauthkeyProvosioner.ephemeral }} - - "--ephemeral" - {{- end }} - {{- with .Values.preauthkeyProvosioner.reusable }} - - "--reusable" - {{- end }} - {{- with .Values.preauthkeyProvosioner.force }} - - "--force" - {{- end }} - {{- with .Values.preauthkeyProvosioner.keyExpiration }} - - "--expiration" - - "{{ $.Values.preauthkeyProvosioner.keyExpiration }}" - {{- end }} - {{- with .Values.preauthkeyProvosioner.tags }} - - "--tags" - - "{{ $.Values.preauthkeyProvosioner.tags }}" - {{- end }} - - "-u" - - "{{ required ".Values.preauthkeyProvosioner.userName missing" .Values.preauthkeyProvosioner.userName }}" - - "--file" - - "/preauthkey/key" - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace - - name: HEADSCALE_CLI_ADDRESS - value: {{ .Values.preauthkeyProvosioner.uri }} - - name: HEADSCALE_CLI_API_KEY - valueFrom: - secretKeyRef: - key: HEADSCALE_CLI_API_KEY - name: {{ .Values.headscale.authkeySecret }} - volumeMounts: - - mountPath: /preauthkey - name: preauthkey - readOnly: false - containers: - - env: - - name: POD_NAMESPACE - valueFrom: - fieldRef: - apiVersion: v1 - fieldPath: metadata.namespace - - name: TS_ACCEPT_DNS - value: "false" - # Store the state in memory instead of a secret - - name: TS_KUBE_SECRET - value: "" - - name: TS_STATE_DIR - value: /preauthkey/state - - name: TS_USERSPACE - value: "true" - - name: TS_EXTRA_ARGS - value: --login-server {{ .Values.headscale.uri }} - - name: TS_TAILSCALED_EXTRA_ARGS - value: '--state=mem: --no-logs-no-support --debug=:8080' - - name: TS_SOCKS5_SERVER - value: :{{ .Values.service.socks5.port }} - - name: TS_OUTBOUND_HTTP_PROXY_LISTEN - value: :{{ .Values.service.httpproxy.port }} - args: - - --socket - - /tmp/tailscaled.sock - image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" - imagePullPolicy: {{ .Values.image.pullPolicy }} - name: {{ .Chart.Name }} - securityContext: - {{- toYaml .Values.securityContext | nindent 12 }} - ports: - {{- range $name, $v := .Values.service }} - - name: {{ $name }} - protocol: TCP - containerPort: {{ $v.port }} - {{- end }} - livenessProbe: - httpGet: - path: /healthz - port: 8090 - initialDelaySeconds: 10 - periodSeconds: 5 - timeoutSeconds: 5 - failureThreshold: 5 - successThreshold: 1 - volumeMounts: - - mountPath: /preauthkey - name: preauthkey - readOnly: false - terminationMessagePath: /dev/termination-log - resources: - {{- toYaml .Values.resources | nindent 12 }} - dnsPolicy: ClusterFirst - securityContext: - {{- toYaml .Values.podSecurityContext | nindent 8 }} - terminationGracePeriodSeconds: 10 - volumes: - - name: preauthkey - emptyDir: - medium: Memory - {{- with .Values.nodeSelector }} - nodeSelector: - {{- toYaml . | nindent 8 }} - {{- end }} - {{- with .Values.tolerations }} - tolerations: - {{- toYaml . | nindent 8 }} - {{- end }} \ No newline at end of file diff --git a/charts/tailscale-proxy/templates/service.yaml b/charts/tailscale-proxy/templates/service.yaml deleted file mode 100644 index 84005abac..000000000 --- a/charts/tailscale-proxy/templates/service.yaml +++ /dev/null @@ -1,22 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -apiVersion: v1 -kind: Service -metadata: - name: {{ include "tailscale-proxy.fullname" . }} - labels: - {{- include "tailscale-proxy.labels" . | nindent 4 }} -spec: - type: {{ .Values.serviceType }} - ports: - {{- range $name, $x := $.Values.service }} - - name: {{ $name }} - port: {{ $x.port }} - targetPort: {{ $x.port }} - protocol: TCP - {{- end }} - selector: - {{- include "tailscale-proxy.selectorLabels" . | nindent 4 }} diff --git a/charts/tailscale-proxy/templates/serviceaccount.yaml b/charts/tailscale-proxy/templates/serviceaccount.yaml deleted file mode 100644 index 0da7f218d..000000000 --- a/charts/tailscale-proxy/templates/serviceaccount.yaml +++ /dev/null @@ -1,17 +0,0 @@ -{{/* -SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -SPDX-License-Identifier: Apache-2.0 -*/}} - -{{- if .Values.serviceAccount.create -}} -apiVersion: v1 -kind: ServiceAccount -metadata: - name: {{ include "tailscale-proxy.serviceAccountName" . }} - labels: - {{- include "tailscale-proxy.labels" . | nindent 4 }} - {{- with .Values.serviceAccount.annotations }} - annotations: - {{- toYaml . | nindent 4 }} - {{- end }} -{{- end }} diff --git a/charts/tailscale-proxy/values.yaml b/charts/tailscale-proxy/values.yaml deleted file mode 100644 index bc68781a7..000000000 --- a/charts/tailscale-proxy/values.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -# SPDX-License-Identifier: Apache-2.0 - -# Default values for tailscale-proxy -# This is a YAML-formatted file. -# Declare variables to be passed into your templates. - -replicaCount: 2 - -image: - repository: ghcr.io/cloudoperators/greenhouse-tailscale - pullPolicy: IfNotPresent - tag: main - -imagePullSecrets: [] -nameOverride: "" -fullnameOverride: "" - -serviceAccount: - # Specifies whether a service account should be created - create: false - # Annotations to add to the service account - annotations: {} - # The name of the service account to use. - # If not set and create is true, a name is generated using the fullname template - name: "" - -podAnnotations: {} - -podSecurityContext: {} - # fsGroup: 2000 - -securityContext: {} - # capabilities: - # drop: - # - ALL - # readOnlyRootFilesystem: true - # runAsNonRoot: true - # runAsUser: 1000 - -serviceType: ClusterIP - -service: - socks5: - port: 1055 - httpproxy: - port: 1066 - metrics: - port: 8080 - -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: [] - -headscale: - # uri: headscale controller url with http:// or https:// - uri: - authkeySecret: tailscale-auth - -preauthkeyProvosioner: - uri: - userName: - # tags: comma separated list of tags - tags: - # keyExpiration: 1h, 1d, ... - keyExpiration: - ephemeral: true - reusable: false - image: - repository: ghcr.io/cloudoperators/greenhouse-headscalectl - pullPolicy: IfNotPresent - tag: main diff --git a/cmd/greenhouse/controllers.go b/cmd/greenhouse/controllers.go index 81983b294..3dd606079 100644 --- a/cmd/greenhouse/controllers.go +++ b/cmd/greenhouse/controllers.go @@ -6,7 +6,6 @@ package main import ( "os" "sort" - "time" ctrl "sigs.k8s.io/controller-runtime" @@ -54,8 +53,7 @@ var knownControllers = map[string]func(controllerName string, mgr ctrl.Manager) "bootStrap": (&clustercontrollers.BootstrapReconciler{}).SetupWithManager, "clusterDirectAccess": startClusterDirectAccessReconciler, // "clusterPropagation": (&clustercontrollers.ClusterPropagationReconciler{}).SetupWithManager, - "clusterHeadscaleAccess": startClusterHeadscaleAccessReconciler, - "clusterStatus": (&clustercontrollers.ClusterStatusReconciler{}).SetupWithManager, + "clusterStatus": (&clustercontrollers.ClusterStatusReconciler{}).SetupWithManager, } // knownControllers lists the name of known controllers. @@ -99,31 +97,3 @@ func startClusterDirectAccessReconciler(name string, mgr ctrl.Manager) error { RenewRemoteClusterBearerTokenAfter: renewRemoteClusterBearerTokenAfter, }).SetupWithManager(name, mgr) } - -func startClusterHeadscaleAccessReconciler(name string, mgr ctrl.Manager) error { - if renewRemoteClusterBearerTokenAfter > remoteClusterBearerTokenValidity { - setupLog.Info("WARN: remoteClusterBearerTokenValidity is less than renewRemoteClusterBearerTokenAfter") - setupLog.Info("Setting renewRemoteClusterBearerTokenAfter to half of remoteClusterBearerTokenValidity") - renewRemoteClusterBearerTokenAfter = remoteClusterBearerTokenValidity / 2 - } - if headscaleAPIKey == "" || headscaleAPIURL == "" { - setupLog.Info("WARN: headscaleApiKey or headscaleApiUrl is not set") - setupLog.Info("Skipping headscale access reconciler") - return nil - } - - if tailscaleProxy == "" { - setupLog.Info("WARN: tailscaleProxy is not set") - setupLog.Info("Skipping headscale access reconciler") - return nil - } - - return (&clustercontrollers.HeadscaleAccessReconciler{ - HeadscaleAPIKey: headscaleAPIKey, - HeadscaleGRPCURL: headscaleAPIURL, - TailscaleProxy: tailscaleProxy, - HeadscalePreAuthenticationKeyMinValidity: 8 * time.Hour, - RemoteClusterBearerTokenValidity: remoteClusterBearerTokenValidity, - RenewRemoteClusterBearerTokenAfter: renewRemoteClusterBearerTokenAfter, - }).SetupWithManager(name, mgr) -} diff --git a/cmd/greenhouse/main.go b/cmd/greenhouse/main.go index 0971ac086..4b0994e38 100644 --- a/cmd/greenhouse/main.go +++ b/cmd/greenhouse/main.go @@ -55,9 +55,6 @@ var ( setupLog = ctrl.Log.WithName("setup") enabledControllers []string - headscaleAPIURL, - headscaleAPIKey, - tailscaleProxy string remoteClusterBearerTokenValidity, renewRemoteClusterBearerTokenAfter time.Duration kubeClientOpts clientutil.RuntimeOptions @@ -85,15 +82,6 @@ func main() { flag.StringVar(&probeAddr, "health-probe-bind-address", ":8081", "The address the probe endpoint binds to.") - flag.StringVar(&headscaleAPIURL, "headscale-api-url", clientutil.GetEnvOrDefault("HEADSCALE_API_URL", ""), - "Headscale API URL.(format https://) Can be set via HEADSCALE_API_URL env var") - - flag.StringVar(&headscaleAPIKey, "headscale-api-key", clientutil.GetEnvOrDefault("HEADSCALE_API_KEY", ""), - "Headscale API KEY. Can be set via HEADSCALE_API_KEY env var") - - flag.StringVar(&tailscaleProxy, "tailscale-proxy", clientutil.GetEnvOrDefault("TAILSCALE_PROXY", ""), - "Tailscale proxy to be used by Greenhouse in case of type the communication is not direct. Can be set via TAILSCALE_PROXY env var") - flag.DurationVar(&remoteClusterBearerTokenValidity, "remote-cluster-bearer-token-validity", defaultRemoteClusterBearerTokenValidity, "Validity of the bearer token we request to access the remote clusters") diff --git a/cmd/headscalectl/main.go b/cmd/headscalectl/main.go deleted file mode 100644 index 0de6adf24..000000000 --- a/cmd/headscalectl/main.go +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package main - -import ( - "github.com/cloudoperators/greenhouse/pkg/headscalectl" -) - -func main() { - headscalectl.Execute() -} diff --git a/cmd/tailscale-starter/main.go b/cmd/tailscale-starter/main.go deleted file mode 100644 index fbdd99dc1..000000000 --- a/cmd/tailscale-starter/main.go +++ /dev/null @@ -1,10 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package main - -import tailscalestarter "github.com/cloudoperators/greenhouse/pkg/tailscale-starter" - -func main() { - tailscalestarter.Execute() -} diff --git a/cmd/tcp-proxy/main.go b/cmd/tcp-proxy/main.go deleted file mode 100644 index d3179c411..000000000 --- a/cmd/tcp-proxy/main.go +++ /dev/null @@ -1,114 +0,0 @@ -/******************************************************************************* -* MIT License -* -* Copyright (c) 2020 dev@jpillora.com -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*******************************************************************************/ - -package main - -import ( - goflag "flag" - "fmt" - "net" - "os" - - "github.com/prometheus/client_golang/prometheus" - flag "github.com/spf13/pflag" - "k8s.io/klog/v2" - - "github.com/cloudoperators/greenhouse/pkg/tcp-proxy/metrics" - "github.com/cloudoperators/greenhouse/pkg/tcp-proxy/proxy" - "github.com/cloudoperators/greenhouse/pkg/version" -) - -var ( - localAddr = flag.String("l", ":443", "local address") - remoteAddr = flag.String("r", fmt.Sprintf("%s:%s", "kubernetes.default.svc.cluster.local", os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")), "remote address") - metricAddress = flag.String("metrics", "127.0.0.1:3002", "IP address and port number to expose prometheus metrics on") - hex = flag.Bool("h", false, "output hex") - unwrapTLS = flag.Bool("unwrap-tls", false, "remote connection with TLS exposed unencrypted locally") -) - -func init() { - prometheus.MustRegister(metrics.InboundConnCounter) - prometheus.MustRegister(metrics.OutboundConnCounter) - prometheus.MustRegister(metrics.InboundBytesCounter) - prometheus.MustRegister(metrics.OutboundBytesCounter) - prometheus.MustRegister(metrics.ActiveInboundConnGauge) - prometheus.MustRegister(metrics.ActiveOutboundConnGauge) -} - -func main() { - goFlagSet := goflag.CommandLine - flag.CommandLine.AddGoFlagSet(goFlagSet) - flag.Parse() - version.ShowVersionAndExit("tcp-proxy") - - klog.Infof("tcp-proxy proxing from %v to %v ", *localAddr, *remoteAddr) - - laddr, err := net.ResolveTCPAddr("tcp", *localAddr) - if err != nil { - klog.Errorf("Failed to resolve local address: %s", err) - os.Exit(1) - } - - _, err = net.LookupIP(*remoteAddr) - if err != nil { - klog.Errorf("Failed to resolve remote address: %s", err) - klog.Infof("Fallback to KUBERNETES_SERVICE_HOST env variable") - *remoteAddr = fmt.Sprintf("%s:%s", os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")) - } - - raddr, err := net.ResolveTCPAddr("tcp", *remoteAddr) - if err != nil { - klog.Errorf("Failed to resolve remote address: %s", err) - os.Exit(1) - } - listener, err := net.ListenTCP("tcp", laddr) - if err != nil { - klog.Errorf("Failed to open local port to listen: %s", err) - os.Exit(1) - } - - prom := metrics.Helper{} - go prom.StartMetricsServer(*metricAddress) - - for { - conn, err := listener.AcceptTCP() - if err != nil { - klog.Errorf("Failed to accept connection '%s'", err) - continue - } - - var p *proxy.Proxy - if *unwrapTLS { - klog.Info("Unwrapping TLS") - p = proxy.NewTLSUnwrapped(conn, laddr, raddr, *remoteAddr) - } else { - p = proxy.New(conn, laddr, raddr) - } - - p.OutputHex = *hex - - metrics.IncrementActiveInboundGauge() - go p.Start() - } -} diff --git a/docs/contribute/local-dev.md b/docs/contribute/local-dev.md index 6354cb948..26e84d101 100644 --- a/docs/contribute/local-dev.md +++ b/docs/contribute/local-dev.md @@ -110,8 +110,6 @@ docker run --network host -e KUBECONFIG=/envtest/kubeconfig -v ./envtest:/envtes See all available flags [here](https://github.com/cloudoperators/greenhouse/blob/main/cmd/greenhouse/main.go#L59-L87). -Note that not setting `headscaleAPIURL`, `headscaleAPIKey` or `tailscaleProxy` will prevent running the [headscale access controller](https://github.com/cloudoperators/greenhouse/blob/main/pkg/controllers/cluster/headscale_access_controller.go). - ## Greenhouse UI Build the latest `dev-ui` image by running: diff --git a/docs/reference/api/index.html b/docs/reference/api/index.html index d39182474..22c97391d 100644 --- a/docs/reference/api/index.html +++ b/docs/reference/api/index.html @@ -730,19 +730,6 @@

ClusterStatus -headScaleStatus
- - -HeadScaleMachineStatus - - - - -

HeadScaleStatus contains the current status of the headscale client.

- - - - statusConditions
@@ -870,113 +857,6 @@

ConditionType Condition)

ConditionType is a valid condition of a resource.

-

HeadScaleMachineStatus -

-

-(Appears on: -ClusterStatus) -

-

HeadScaleMachineStatus is the status of a Headscale machine.

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescription
-id
- -uint64 - -
-
-ipAddresses
- -[]string - -
-
-name
- -string - -
-
-expiry
- - -Kubernetes meta/v1.Time - - -
-
-createdAt
- - -Kubernetes meta/v1.Time - - -
-
-forcedTags
- -[]string - -
-
-preAuthKey
- - -PreAuthKey - - -
-
-online
- -bool - -
-
-
-

HelmChartReference

@@ -2391,101 +2271,6 @@

PluginStatus -

PreAuthKey -

-

-(Appears on: -HeadScaleMachineStatus) -

-

PreAuthKey reflects the status of the pre-authentication key used by the Headscale machine.

-
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
FieldDescription
-id
- -string - -
-
-user
- -string - -
-
-reusable
- -bool - -
-
-ephemeral
- -bool - -
-
-used
- -bool - -
-
-createdAt
- - -Kubernetes meta/v1.Time - - -
-
-expiration
- - -Kubernetes meta/v1.Time - - -
-
-
-

PropagationStatus

diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index dc5e3752e..43e51dabb 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Greenhouse - version: 2fe4054 + version: b6c8a6d description: PlusOne operations platform paths: /Organization: @@ -9,21 +9,21 @@ paths: responses: default: description: Organization - /Plugin: + /ClusterKubeconfig: post: responses: default: - description: Plugin + description: ClusterKubeconfig /Cluster: post: responses: default: description: Cluster - /ClusterKubeconfig: + /TeamRole: post: responses: default: - description: ClusterKubeconfig + description: TeamRole /TeamMembership: post: responses: @@ -34,26 +34,26 @@ paths: responses: default: description: PluginPreset - /PluginDefinition: + /Team: post: responses: default: - description: PluginDefinition - /TeamRole: + description: Team + /TeamRoleBinding: post: responses: default: - description: TeamRole - /Team: + description: TeamRoleBinding + /Plugin: post: responses: default: - description: Team - /TeamRoleBinding: + description: Plugin + /PluginDefinition: post: responses: default: - description: TeamRoleBinding + description: PluginDefinition components: schemas: Organization: @@ -177,12 +177,12 @@ components: description: OrganizationStatus defines the observed state of an Organization type: object type: object - Plugin: + ClusterKubeconfig: xml: name: greenhouse.sap namespace: v1alpha1 - title: Plugin - description: Plugin is the Schema for the plugins API + title: ClusterKubeconfig + description: ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -193,179 +193,92 @@ components: metadata: type: object spec: - description: PluginSpec defines the desired state of Plugin + description: ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig properties: - clusterName: - description: ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. - type: string - disabled: - description: Disabled indicates that the plugin is administratively disabled. - type: boolean - displayName: - description: DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. - type: string - optionValues: - description: Values are the values for a PluginDefinition instance. - items: - description: PluginOptionValue is the value for a PluginOption. - properties: - name: - description: Name of the values. - type: string - value: - description: Value is the actual value in plain text. - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: ValueFrom references a potentially confidential value in another source. + kubeconfig: + description: 'ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config' + properties: + apiVersion: + type: string + clusters: + items: properties: - secret: - description: Secret references the secret containing the value. + cluster: properties: - key: - description: Key in the secret to select the value from. + certificate-authority-data: + format: byte type: string - name: - description: Name of the secret in the same namespace. + server: + type: string + type: object + name: + type: string + required: + - cluster + - name + type: object + type: array + contexts: + items: + properties: + context: + properties: + cluster: + type: string + namespace: + type: string + user: type: string required: - - key - - name + - cluster + - user type: object + name: + type: string + required: + - name type: object - required: - - name - type: object - type: array - pluginDefinition: - description: PluginDefinition is the name of the PluginDefinition this instance is for. - type: string - releaseNamespace: - description: ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. - type: string - required: - - disabled - - pluginDefinition - type: object - status: - description: PluginStatus defines the observed state of Plugin - properties: - description: - description: Description provides additional details of the plugin. - type: string - exposedServices: - additionalProperties: - description: Service references a Kubernetes service of a Plugin. - properties: - name: - description: Name is the name of the service in the target cluster. - type: string - namespace: - description: Namespace is the namespace of the service in the target cluster. - type: string - port: - description: Port is the port of the service. - format: int32 - type: integer - protocol: - description: Protocol is the protocol of the service. - type: string - required: - - name - - namespace - - port - type: object - description: ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. - type: object - helmChart: - description: HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. - properties: - name: - description: Name of the HelmChart chart. - type: string - repository: - description: Repository of the HelmChart chart. - type: string - version: - description: Version of the HelmChart chart. - type: string - required: - - name - - repository - - version - type: object - helmReleaseStatus: - description: HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. - properties: - firstDeployed: - description: FirstDeployed is the timestamp of the first deployment of the release. - format: date-time - type: string - lastDeployed: - description: LastDeployed is the timestamp of the last deployment of the release. - format: date-time + type: array + current-context: type: string - status: - description: Status is the status of a HelmChart release. + kind: type: string - required: - - status - type: object - statusConditions: - description: StatusConditions contain the different conditions that constitute the status of the Plugin. - properties: - conditions: + preferences: + type: object + users: items: - description: Condition contains additional information on the state of a resource. properties: - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - message: - description: Message is an optional human readable message indicating details about the last transition. - type: string - reason: - description: Reason is a one-word, CamelCase reason for the condition's last transition. - type: string - status: - description: Status of the condition. - type: string - type: - description: Type of the condition. + name: type: string + user: + properties: + auth-provider: + description: AuthProviderConfig holds the configuration for a specified auth provider. + properties: + config: + additionalProperties: + type: string + type: object + name: + type: string + required: + - name + type: object + client-certificate-data: + format: byte + type: string + client-key-data: + format: byte + type: string + type: object required: - - lastTransitionTime - - status - - type + - name type: object type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map - type: object - uiApplication: - description: UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. - properties: - name: - description: Name of the UI application. - type: string - url: - description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. - type: string - version: - description: Version of the frontend application. - type: string required: - - name - - version + - contexts + - users type: object - version: - description: Version contains the latest pluginDefinition version the config was last applied with successfully. - type: string - weight: - description: Weight configures the order in which Plugins are shown in the Greenhouse UI. - format: int32 - type: integer type: object type: object Cluster: @@ -390,7 +303,6 @@ components: description: AccessMode configures how the cluster is accessed from the Greenhouse operator. enum: - direct - - headscale type: string required: - accessMode @@ -402,51 +314,6 @@ components: description: BearerTokenExpirationTimestamp reflects the expiration timestamp of the bearer token used to access the cluster. format: date-time type: string - headScaleStatus: - description: HeadScaleStatus contains the current status of the headscale client. - properties: - createdAt: - format: date-time - type: string - expiry: - format: date-time - type: string - forcedTags: - items: - type: string - type: array - id: - format: int64 - type: integer - ipAddresses: - items: - type: string - type: array - name: - type: string - online: - type: boolean - preAuthKey: - description: PreAuthKey reflects the status of the pre-authentication key used by the Headscale machine. - properties: - createdAt: - format: date-time - type: string - ephemeral: - type: boolean - expiration: - format: date-time - type: string - id: - type: string - reusable: - type: boolean - used: - type: boolean - user: - type: string - type: object - type: object kubernetesVersion: description: KubernetesVersion reflects the detected Kubernetes version of the cluster. type: string @@ -527,12 +394,12 @@ components: type: object type: object type: object - ClusterKubeconfig: + TeamRole: xml: name: greenhouse.sap namespace: v1alpha1 - title: ClusterKubeconfig - description: ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name + title: TeamRole + description: TeamRole is the Schema for the TeamRoles API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -543,92 +410,96 @@ components: metadata: type: object spec: - description: ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig + description: TeamRoleSpec defines the desired state of a TeamRole properties: - kubeconfig: - description: 'ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config' + aggregationRule: + description: AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster properties: - apiVersion: - type: string - clusters: - items: - properties: - cluster: - properties: - certificate-authority-data: - format: byte - type: string - server: - type: string - type: object - name: - type: string - required: - - cluster - - name - type: object - type: array - contexts: - items: - properties: - context: - properties: - cluster: - type: string - namespace: - type: string - user: - type: string - required: - - cluster - - user - type: object - name: - type: string - required: - - name - type: object - type: array - current-context: - type: string - kind: - type: string - preferences: - type: object - users: + clusterRoleSelectors: + description: ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added items: + description: A label selector is a label query over a set of resources. The result of matchLabels and\nmatchExpressions are ANDed. An empty label selector matches all objects. A null\nlabel selector matches no objects. properties: - name: - type: string - user: - properties: - auth-provider: - description: AuthProviderConfig holds the configuration for a specified auth provider. - properties: - config: - additionalProperties: - type: string - type: object - name: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. + items: type: string - required: - - name - type: object - client-certificate-data: - format: byte - type: string - client-key-data: - format: byte - type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. type: object - required: - - name type: object + x-kubernetes-map-type: atomic type: array - required: - - contexts - - users + x-kubernetes-list-type: atomic + type: object + labels: + additionalProperties: + type: string + description: Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles type: object + rules: + description: Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role + items: + description: PolicyRule holds information that describes a policy rule, but does not contain information\nabout who the rule applies to or which namespace the rule applies to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + items: + type: string + type: array + x-kubernetes-list-type: atomic + nonResourceURLs: + description: NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resourceNames: + description: ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resources: + description: Resources is a list of resources this rule applies to. '*' represents all resources. + items: + type: string + type: array + x-kubernetes-list-type: atomic + verbs: + description: Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - verbs + type: object + type: array + type: object + status: + description: TeamRoleStatus defines the observed state of a TeamRole type: object type: object TeamMembership: @@ -910,12 +781,12 @@ components: type: object type: object type: object - PluginDefinition: + Team: xml: name: greenhouse.sap namespace: v1alpha1 - title: PluginDefinition - description: PluginDefinition is the Schema for the PluginDefinitions API + title: Team + description: Team is the Schema for the teams API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -926,265 +797,42 @@ components: metadata: type: object spec: - description: PluginDefinitionSpec defines the desired state of PluginDefinitionSpec + description: TeamSpec defines the desired state of Team properties: description: - description: Description provides additional details of the pluginDefinition. + description: Description provides additional details of the team. type: string - displayName: - description: DisplayName provides a human-readable label for the pluginDefinition. + mappedIdPGroup: + description: IdP group id matching team. type: string - docMarkDownUrl: - description: DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. + type: object + status: + description: TeamStatus defines the observed state of Team + type: object + type: object + TeamRoleBinding: + xml: + name: greenhouse.sap + namespace: v1alpha1 + title: TeamRoleBinding + description: TeamRoleBinding is the Schema for the rolebindings API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TeamRoleBindingSpec defines the desired state of a TeamRoleBinding + properties: + clusterName: + description: ClusterName is the name of the cluster the rbacv1 resources are created on. type: string - helmChart: - description: HelmChart specifies where the Helm Chart for this pluginDefinition can be found. - properties: - name: - description: Name of the HelmChart chart. - type: string - repository: - description: Repository of the HelmChart chart. - type: string - version: - description: Version of the HelmChart chart. - type: string - required: - - name - - repository - - version - type: object - icon: - description: 'Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px' - type: string - options: - description: RequiredValues is a list of values required to create an instance of this PluginDefinition. - items: - properties: - default: - description: Default provides a default value for the option - x-kubernetes-preserve-unknown-fields: true - description: - description: Description provides a human-readable text for the value as shown in the UI. - type: string - displayName: - description: DisplayName provides a human-readable label for the configuration option - type: string - name: - description: Name/Key of the config option. - type: string - regex: - description: Regex specifies a match rule for validating configuration options. - type: string - required: - description: Required indicates that this config option is required - type: boolean - type: - description: Type of this configuration option. - enum: - - string - - secret - - bool - - int - - list - - map - type: string - required: - - name - - required - - type - type: object - type: array - uiApplication: - description: UIApplication specifies a reference to a UI application - properties: - name: - description: Name of the UI application. - type: string - url: - description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. - type: string - version: - description: Version of the frontend application. - type: string - required: - - name - - version - type: object - version: - description: Version of this pluginDefinition - type: string - weight: - description: Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. - format: int32 - type: integer - required: - - version - type: object - status: - description: PluginDefinitionStatus defines the observed state of PluginDefinition - type: object - type: object - TeamRole: - xml: - name: greenhouse.sap - namespace: v1alpha1 - title: TeamRole - description: TeamRole is the Schema for the TeamRoles API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TeamRoleSpec defines the desired state of a TeamRole - properties: - aggregationRule: - description: AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster - properties: - clusterRoleSelectors: - description: ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added - items: - description: A label selector is a label query over a set of resources. The result of matchLabels and\nmatchExpressions are ANDed. An empty label selector matches all objects. A null\nlabel selector matches no objects. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-map-type: atomic - type: array - x-kubernetes-list-type: atomic - type: object - labels: - additionalProperties: - type: string - description: Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles - type: object - rules: - description: Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role - items: - description: PolicyRule holds information that describes a policy rule, but does not contain information\nabout who the rule applies to or which namespace the rule applies to. - properties: - apiGroups: - description: APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. - items: - type: string - type: array - x-kubernetes-list-type: atomic - nonResourceURLs: - description: NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. - items: - type: string - type: array - x-kubernetes-list-type: atomic - resourceNames: - description: ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. - items: - type: string - type: array - x-kubernetes-list-type: atomic - resources: - description: Resources is a list of resources this rule applies to. '*' represents all resources. - items: - type: string - type: array - x-kubernetes-list-type: atomic - verbs: - description: Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - verbs - type: object - type: array - type: object - status: - description: TeamRoleStatus defines the observed state of a TeamRole - type: object - type: object - Team: - xml: - name: greenhouse.sap - namespace: v1alpha1 - title: Team - description: Team is the Schema for the teams API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TeamSpec defines the desired state of Team - properties: - description: - description: Description provides additional details of the team. - type: string - mappedIdPGroup: - description: IdP group id matching team. - type: string - type: object - status: - description: TeamStatus defines the observed state of Team - type: object - type: object - TeamRoleBinding: - xml: - name: greenhouse.sap - namespace: v1alpha1 - title: TeamRoleBinding - description: TeamRoleBinding is the Schema for the rolebindings API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TeamRoleBindingSpec defines the desired state of a TeamRoleBinding - properties: - clusterName: - description: ClusterName is the name of the cluster the rbacv1 resources are created on. - type: string - clusterSelector: - description: ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to. + clusterSelector: + description: ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to. properties: matchExpressions: description: matchExpressions is a list of label selector requirements. The requirements are ANDed. @@ -1305,3 +953,309 @@ components: type: object type: object type: object + Plugin: + xml: + name: greenhouse.sap + namespace: v1alpha1 + title: Plugin + description: Plugin is the Schema for the plugins API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PluginSpec defines the desired state of Plugin + properties: + clusterName: + description: ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. + type: string + disabled: + description: Disabled indicates that the plugin is administratively disabled. + type: boolean + displayName: + description: DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. + type: string + optionValues: + description: Values are the values for a PluginDefinition instance. + items: + description: PluginOptionValue is the value for a PluginOption. + properties: + name: + description: Name of the values. + type: string + value: + description: Value is the actual value in plain text. + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: ValueFrom references a potentially confidential value in another source. + properties: + secret: + description: Secret references the secret containing the value. + properties: + key: + description: Key in the secret to select the value from. + type: string + name: + description: Name of the secret in the same namespace. + type: string + required: + - key + - name + type: object + type: object + required: + - name + type: object + type: array + pluginDefinition: + description: PluginDefinition is the name of the PluginDefinition this instance is for. + type: string + releaseNamespace: + description: ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. + type: string + required: + - disabled + - pluginDefinition + type: object + status: + description: PluginStatus defines the observed state of Plugin + properties: + description: + description: Description provides additional details of the plugin. + type: string + exposedServices: + additionalProperties: + description: Service references a Kubernetes service of a Plugin. + properties: + name: + description: Name is the name of the service in the target cluster. + type: string + namespace: + description: Namespace is the namespace of the service in the target cluster. + type: string + port: + description: Port is the port of the service. + format: int32 + type: integer + protocol: + description: Protocol is the protocol of the service. + type: string + required: + - name + - namespace + - port + type: object + description: ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. + type: object + helmChart: + description: HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. + properties: + name: + description: Name of the HelmChart chart. + type: string + repository: + description: Repository of the HelmChart chart. + type: string + version: + description: Version of the HelmChart chart. + type: string + required: + - name + - repository + - version + type: object + helmReleaseStatus: + description: HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. + properties: + firstDeployed: + description: FirstDeployed is the timestamp of the first deployment of the release. + format: date-time + type: string + lastDeployed: + description: LastDeployed is the timestamp of the last deployment of the release. + format: date-time + type: string + status: + description: Status is the status of a HelmChart release. + type: string + required: + - status + type: object + statusConditions: + description: StatusConditions contain the different conditions that constitute the status of the Plugin. + properties: + conditions: + items: + description: Condition contains additional information on the state of a resource. + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition transitioned from one status to another. + format: date-time + type: string + message: + description: Message is an optional human readable message indicating details about the last transition. + type: string + reason: + description: Reason is a one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status of the condition. + type: string + type: + description: Type of the condition. + type: string + required: + - lastTransitionTime + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + uiApplication: + description: UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. + properties: + name: + description: Name of the UI application. + type: string + url: + description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. + type: string + version: + description: Version of the frontend application. + type: string + required: + - name + - version + type: object + version: + description: Version contains the latest pluginDefinition version the config was last applied with successfully. + type: string + weight: + description: Weight configures the order in which Plugins are shown in the Greenhouse UI. + format: int32 + type: integer + type: object + type: object + PluginDefinition: + xml: + name: greenhouse.sap + namespace: v1alpha1 + title: PluginDefinition + description: PluginDefinition is the Schema for the PluginDefinitions API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: PluginDefinitionSpec defines the desired state of PluginDefinitionSpec + properties: + description: + description: Description provides additional details of the pluginDefinition. + type: string + displayName: + description: DisplayName provides a human-readable label for the pluginDefinition. + type: string + docMarkDownUrl: + description: DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. + type: string + helmChart: + description: HelmChart specifies where the Helm Chart for this pluginDefinition can be found. + properties: + name: + description: Name of the HelmChart chart. + type: string + repository: + description: Repository of the HelmChart chart. + type: string + version: + description: Version of the HelmChart chart. + type: string + required: + - name + - repository + - version + type: object + icon: + description: 'Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px' + type: string + options: + description: RequiredValues is a list of values required to create an instance of this PluginDefinition. + items: + properties: + default: + description: Default provides a default value for the option + x-kubernetes-preserve-unknown-fields: true + description: + description: Description provides a human-readable text for the value as shown in the UI. + type: string + displayName: + description: DisplayName provides a human-readable label for the configuration option + type: string + name: + description: Name/Key of the config option. + type: string + regex: + description: Regex specifies a match rule for validating configuration options. + type: string + required: + description: Required indicates that this config option is required + type: boolean + type: + description: Type of this configuration option. + enum: + - string + - secret + - bool + - int + - list + - map + type: string + required: + - name + - required + - type + type: object + type: array + uiApplication: + description: UIApplication specifies a reference to a UI application + properties: + name: + description: Name of the UI application. + type: string + url: + description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. + type: string + version: + description: Version of the frontend application. + type: string + required: + - name + - version + type: object + version: + description: Version of this pluginDefinition + type: string + weight: + description: Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. + format: int32 + type: integer + required: + - version + type: object + status: + description: PluginDefinitionStatus defines the observed state of PluginDefinition + type: object + type: object diff --git a/docs/user-guides/cluster/onboarding.md b/docs/user-guides/cluster/onboarding.md index c8ff44e6f..39b3c1967 100644 --- a/docs/user-guides/cluster/onboarding.md +++ b/docs/user-guides/cluster/onboarding.md @@ -36,6 +36,7 @@ greenhousectl cluster bootstrap --bootstrap-kubeconfig 0 && machine.User != nil && machine.User.Name == headscaleKeyForCluster(cluster) { - onlineMachineList = append(onlineMachineList, machine) - } - } - // Sort by ID highest to lowest to have the most recent machine first in the list. - sort.SliceStable(onlineMachineList, func(i, j int) bool { - return onlineMachineList[i].Id > onlineMachineList[j].Id - }) - return onlineMachineList, nil -} - -func (r *HeadscaleAccessReconciler) getIPAddressForHeadscaleClientInRemoteCluster(ctx context.Context, cluster *greenhousev1alpha1.Cluster) (string, error) { - machineList, err := r.getOnlineHeadscaleMachinesForRemoteCluster(ctx, cluster) - if err != nil { - return "", err - } - for _, machine := range machineList { - if len(machine.IpAddresses) > 0 { - return machine.IpAddresses[0], nil - } - } - return "", nil -} - -func (r *HeadscaleAccessReconciler) reconcileServiceAccountToken(ctx context.Context, restClientGetter *clientutil.RestClientGetter, cluster *greenhousev1alpha1.Cluster) error { - ipAddress, err := r.getIPAddressForHeadscaleClientInRemoteCluster(ctx, cluster) - if err != nil { - return err - } - var tokenRequestor = &tokenHelper{ - Client: r.Client, - Proxy: r.TailscaleProxy, - HeadscaleAddress: ipAddress, - RemoteClusterBearerTokenValidity: r.RemoteClusterBearerTokenValidity, - RenewRemoteClusterBearerTokenAfter: r.RenewRemoteClusterBearerTokenAfter, - } - return tokenRequestor.ReconcileServiceAccountToken(ctx, restClientGetter, cluster) -} - -// reconcileStatus updates the cluster status for transparency purposes. -// In contrast to other reconciliation functions, errors don't end the flow to ensure the status is always reported. -func (r *HeadscaleAccessReconciler) reconcileStatus(ctx context.Context, cluster *greenhousev1alpha1.Cluster) error { - var ( - headscaleMachineStatus = new(greenhousev1alpha1.HeadScaleMachineStatus) - headscaleReadyCondition = greenhousev1alpha1.UnknownCondition(greenhousev1alpha1.HeadscaleReady, "", "") - ) - if machineList, err := r.getOnlineHeadscaleMachinesForRemoteCluster(ctx, cluster); err == nil && len(machineList) > 0 { - // TODO: I adapted the status collection from the initial implementation to have the tests green. - // Why is only the first machine considered though? - firstMachine := machineList[0] - headscaleMachineStatus.ID = firstMachine.GetId() - headscaleMachineStatus.IPAddresses = firstMachine.GetIpAddresses() - headscaleMachineStatus.Name = firstMachine.GetName() - headscaleMachineStatus.ForcedTags = firstMachine.GetForcedTags() - headscaleMachineStatus.Online = firstMachine.GetOnline() - if expirationTime := firstMachine.GetExpiry(); expirationTime != nil { - headscaleMachineStatus.Expiry = metav1.NewTime(expirationTime.AsTime()) - } - if createdAtTime := firstMachine.GetCreatedAt(); createdAtTime != nil { - headscaleMachineStatus.CreatedAt = metav1.NewTime(createdAtTime.AsTime()) - } - if preAuthKey := firstMachine.PreAuthKey; preAuthKey != nil { - headscaleMachineStatus.PreAuthKey = &greenhousev1alpha1.PreAuthKey{ - ID: preAuthKey.GetId(), - User: preAuthKey.GetUser(), - Reusable: preAuthKey.GetReusable(), - Ephemeral: preAuthKey.GetEphemeral(), - Used: preAuthKey.GetUsed(), - } - if createdAtTime := preAuthKey.GetCreatedAt(); createdAtTime != nil { - headscaleMachineStatus.PreAuthKey.CreatedAt = metav1.NewTime(createdAtTime.AsTime()) - } - if expirationTime := preAuthKey.GetExpiration(); expirationTime != nil { - headscaleMachineStatus.PreAuthKey.Expiration = metav1.NewTime(expirationTime.AsTime()) - } - } - headscaleReadyCondition.Status = metav1.ConditionTrue - } else if len(machineList) == 0 { - headscaleReadyCondition.Status = metav1.ConditionFalse - headscaleReadyCondition.Message = "no headscale machine found" - log.FromContext(ctx).Error(nil, "no headscale machine found", "cluster", cluster.GetName(), "namespace", cluster.GetNamespace()) - } else { - headscaleReadyCondition.Status = metav1.ConditionFalse - headscaleReadyCondition.Message = err.Error() - log.FromContext(ctx).Error(err, "failed to get headscale machine status") - } - - var kubernetesVersion = "unknown" - if restClientGetterForRemoteCluster, err := r.newRestClientGetterForCluster(ctx, cluster); err == nil { - // Get the kubernetes version of the remote cluster. - if k8sVersion, err := clientutil.GetKubernetesVersion(restClientGetterForRemoteCluster); err == nil { - kubernetesVersion = k8sVersion.String() - } else { - log.FromContext(ctx).Error(err, "failed to get cluster version") - } - } else { - log.FromContext(ctx).Error(err, "failed to get rest client getter for cluster") - } - - _, err := clientutil.PatchStatus(ctx, r.Client, cluster, func() error { - cluster.Status.KubernetesVersion = kubernetesVersion - cluster.Status.HeadScaleStatus = headscaleMachineStatus - cluster.Status.SetConditions(headscaleReadyCondition) - return nil - }) - return err -} - -// deleteHeadscaleMachine deletes all headscale machines for the given cluster and returns true if none can be found. -// Note: This function is meant to be re-tried on false or an error. -func (r *HeadscaleAccessReconciler) deleteHeadscaleMachine(ctx context.Context, cluster *greenhousev1alpha1.Cluster) (bool, error) { - machinesToDelete, err := r.headscaleGRPCClient.ListMachines(ctx, &headscalev1.ListMachinesRequest{ - User: headscaleKeyForCluster(cluster), - }) - if err != nil { - return false, err - } - // Report done if there's no machine associated with the cluster. - if len(machinesToDelete.GetMachines()) == 0 { - return true, nil - } - allErrs := make([]error, 0) - for _, machine := range machinesToDelete.GetMachines() { - if _, err := r.headscaleGRPCClient.DeleteMachine(ctx, &headscalev1.DeleteMachineRequest{ - MachineId: machine.GetId(), - }); err != nil { - allErrs = append(allErrs, err) - } - } - // We return false here to indicate this function should be called again as deleting machines in Headscale might take a while. - return false, utilerrors.NewAggregate(allErrs) -} - -func (r *HeadscaleAccessReconciler) deleteHeadscaleUser(ctx context.Context, cluster *greenhousev1alpha1.Cluster) (bool, error) { - // Check if the user still exists. - _, err := r.headscaleGRPCClient.GetUser(ctx, &headscalev1.GetUserRequest{ - Name: headscaleKeyForCluster(cluster), - }) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return false, err - } - if strings.Contains(errStatus.Message(), "not found") { - return true, nil - } - return false, err - } - // Attempt deletion of the user. - if _, err := r.headscaleGRPCClient.DeleteUser(ctx, &headscalev1.DeleteUserRequest{ - Name: headscaleKeyForCluster(cluster), - }); err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return false, err - } - if strings.Contains(errStatus.Message(), "not found") { - return true, nil - } - return false, err - } - return true, nil -} - -func (r *HeadscaleAccessReconciler) deleteResourcesOnRemoteCluster(ctx context.Context, cluster *greenhousev1alpha1.Cluster) (bool, error) { - ipAddress, err := r.getIPAddressForHeadscaleClientInRemoteCluster(ctx, cluster) - if err != nil { - return false, err - } - restClientGetter, err := r.newRestClientGetterForCluster(ctx, cluster) - if err != nil { - return false, err - } - k8sHeadscaleProxyClientForRemoteCluster, err := r.getHeadscaleClientFromRestClientGetter(restClientGetter, r.TailscaleProxy, ipAddress) - if err != nil { - return false, err - } - // all remote resources are bound by owner-reference to the namespace - if err := deleteNamespaceInRemoteCluster(ctx, k8sHeadscaleProxyClientForRemoteCluster, cluster); err != nil { - return false, err - } - return true, nil -} - -func (r *HeadscaleAccessReconciler) newRestClientGetterForCluster(ctx context.Context, cluster *greenhousev1alpha1.Cluster) (*clientutil.RestClientGetter, error) { - var clusterSecret = new(corev1.Secret) - if err := r.Get(ctx, types.NamespacedName{Name: cluster.GetSecretName(), Namespace: cluster.GetNamespace()}, clusterSecret); err != nil { - return nil, err - } - return clientutil.NewRestClientGetterFromSecret(clusterSecret, cluster.Namespace) -} - -// headscaleKeyForCluster returns the key for the given cluster to use in headscale. -func headscaleKeyForCluster(cluster *greenhousev1alpha1.Cluster) string { - return fmt.Sprintf("%s-%s", cluster.GetNamespace(), cluster.GetName()) -} - -// isPreAuthenticationKeyIsNotExpired returns true if the given pre-authentication key is valid and not yet expired. -func isPreAuthenticationKeyIsNotExpired(preAuthenticationKey *headscalev1.PreAuthKey, preAuthenticationKeyMinValidity time.Duration) bool { - if preAuthenticationKey.Expiration == nil { - return false - } - return preAuthenticationKey.Expiration.IsValid() && - preAuthenticationKey.Expiration.AsTime().After(time.Now().Add(preAuthenticationKeyMinValidity)) -} diff --git a/pkg/controllers/cluster/headscale_access_controller_test.go b/pkg/controllers/cluster/headscale_access_controller_test.go deleted file mode 100644 index e395638ab..000000000 --- a/pkg/controllers/cluster/headscale_access_controller_test.go +++ /dev/null @@ -1,193 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package cluster_test - -import ( - "strings" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - rbacv1 "k8s.io/api/rbac/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - client "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" - - greenhouseapis "github.com/cloudoperators/greenhouse/pkg/apis" - greenhousev1alpha1 "github.com/cloudoperators/greenhouse/pkg/apis/greenhouse/v1alpha1" - "github.com/cloudoperators/greenhouse/pkg/clientutil" - clusterpkg "github.com/cloudoperators/greenhouse/pkg/controllers/cluster" - "github.com/cloudoperators/greenhouse/pkg/test" -) - -var _ = Describe("Reconciling a Headscale Cluster with mocked Headscale GRPC client and swapped client getter", Ordered, func() { - const ( - headscaleTestCase = "headscale-access" - ) - - var ( - cluster *greenhousev1alpha1.Cluster - - remoteEnvTest *envtest.Environment - remoteClient client.Client - setup *test.TestSetup - ) - - BeforeAll(func() { - var remoteKubeConfig []byte - _, remoteClient, remoteEnvTest, remoteKubeConfig = test.StartControlPlane("6887", false, false) - - // inject the fake headscale client getter - clusterpkg.ExportSetRestClientGetterFunc(headscaleReconciler, newFakeHeadscaleClientGetter(remoteClient)) - - setup = test.NewTestSetup(test.Ctx, test.K8sClient, headscaleTestCase) - - // create a greenhouse cluster with headscale access type - cluster = setup.CreateCluster(test.Ctx, headscaleTestCase, test.WithAccessMode(greenhousev1alpha1.ClusterAccessModeHeadscale)) - - setup.CreateSecret(test.Ctx, headscaleTestCase, - test.WithSecretType(greenhouseapis.SecretTypeKubeConfig), - test.WithSecretData(map[string][]byte{greenhouseapis.KubeConfigKey: remoteKubeConfig})) - - expectedOwnerReference := metav1.OwnerReference{ - Kind: "Cluster", - APIVersion: "greenhouse.sap/v1alpha1", - UID: cluster.UID, - Name: cluster.Name, - } - secret := corev1.Secret{} - Eventually(func(g Gomega) bool { - g.Expect(test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: cluster.Name, Namespace: setup.Namespace()}, &secret)).To(Succeed()) - g.Expect(secret.ObjectMeta.OwnerReferences).To(ContainElement(expectedOwnerReference), "the kubeconfig secret should have an owner reference to the cluster") - return true - }).Should(BeTrue(), "eventually the secret should have an owner reference to the cluster") - - }) - AfterAll(func() { - test.MustDeleteCluster(test.Ctx, test.K8sClient, types.NamespacedName{Name: cluster.Name, Namespace: setup.Namespace()}) - Expect(remoteEnvTest.Stop()).To(Succeed(), "there must be no error stopping the remote environment") - }) - - It("should reconcile headscale cluster", func() { - By("Checking the Headscale Status is being set in the local cluster") - getCluster := &greenhousev1alpha1.Cluster{} - id := types.NamespacedName{Name: cluster.Name, Namespace: setup.Namespace()} - Eventually(func(g Gomega) bool { - g.Expect(test.K8sClient.Get(test.Ctx, id, getCluster)).To(Succeed(), "There should be no error getting the cluster") - g.Expect(getCluster.Spec.AccessMode).To(Equal(greenhousev1alpha1.ClusterAccessModeHeadscale), "The cluster access mode should be set to headscale") - g.Expect(getCluster.Status.HeadScaleStatus).ToNot(BeNil(), "headscale status should be set") - headscaleCondition := getCluster.Status.GetConditionByType(greenhousev1alpha1.HeadscaleReady) - g.Expect(headscaleCondition).ToNot(BeNil(), "The HeadscaleReady condition should be present") - g.Expect(headscaleCondition.Status).To(Equal(metav1.ConditionTrue), "The HeadscaleReady condition status should be true") - return true - }). - Should(BeTrue(), "getting the cluster should succeed eventually and status should be set correctly") - - By("Checking the Namespace is created in the Remote Cluster") - getNamespace := &corev1.Namespace{} - id = types.NamespacedName{Name: setup.Namespace()} - Eventually(func(g Gomega) bool { - g.Expect(remoteClient.Get(test.Ctx, id, getNamespace)).To(Succeed(), "There should be no error getting the remote namespace") - g.Expect(getNamespace.GetName()).To(Equal(setup.Namespace()), "The remote namespace name should be correct") - g.Expect(getNamespace.Status.Phase).To(Equal(corev1.NamespaceActive), "The remote namespace should be active") - return true - }).Should(BeTrue(), "getting the namespace should succeed eventually") - - By("Checking the Service Account is created in the Remote Cluster") - getServiceAccount := &corev1.ServiceAccount{} - id = types.NamespacedName{Name: clusterpkg.ExportServiceAccountName, Namespace: setup.Namespace()} - Eventually(func(g Gomega) bool { - g.Expect(remoteClient.Get(test.Ctx, id, getServiceAccount)).To(Succeed(), "There should be no error getting the remote service account") - g.Expect(getServiceAccount.GetName()).To(Equal(clusterpkg.ExportServiceAccountName), "The SA name should be correct") - g.Expect(getServiceAccount.Namespace).To(Equal(setup.Namespace()), "The SA should be deployed to the correct namespace") - return true - }).Should(BeTrue(), "getting the service account should succeed eventually") - - By("Checking the Cluster Role Binding is created in the Remote Cluster") - getClusterRoleBinding := &rbacv1.ClusterRoleBinding{} - id = types.NamespacedName{Name: "greenhouse"} - Eventually(func(g Gomega) bool { - g.Expect(remoteClient.Get(test.Ctx, id, getClusterRoleBinding)).To(Succeed(), "There should be no error getting the remote crb") - g.Expect(getClusterRoleBinding.RoleRef.Name).To(Equal("cluster-admin"), "crb should bind cluster-admin") - g.Expect(getClusterRoleBinding.Subjects[0].Namespace).To(Equal(setup.Namespace()), "crb should be deployed to correct namespace") - g.Expect(getClusterRoleBinding.OwnerReferences[0].Name).To(Equal(setup.Namespace()), "crb should have owner-reference to namespace") - return true - }).Should(BeTrue(), "getting the cluster role binding should succeed eventually") - - By("Checking the Service Account Token is updated in the Local Cluster") - getSecret := &corev1.Secret{} - id = types.NamespacedName{Name: cluster.Name, Namespace: setup.Namespace()} - Eventually(func(g Gomega) bool { - g.Expect(test.K8sClient.Get(test.Ctx, id, getSecret)).To(Succeed(), "There should be no error getting the cluster secret") - actConfig, ok := getSecret.Data[greenhouseapis.GreenHouseKubeConfigKey] - if !ok { - return false - } - g.Expect(strings.Contains(string(actConfig), tailscaleProxyURL)).To(BeTrue(), "The secret should contain the proxy url") - return true - }).Should(BeTrue(), "getting the secret should succeed eventually and the secret should contain the proxy url") - - By("Checking the Headscale PreAuthKey is set in the secret in the remote cluster") - getSecret = &corev1.Secret{} - id = types.NamespacedName{Name: "tailscale-auth", Namespace: setup.Namespace()} - Eventually(func(g Gomega) bool { - g.Expect(remoteClient.Get(test.Ctx, id, getSecret)).To(Succeed(), "There should be no error getting the remote secret") - actConfig, ok := getSecret.Data[clusterpkg.ExportTailscaleAuthorizationKey] - if !ok { - return false - } - g.Expect(actConfig).ToNot(BeNil(), "The secret should containt the preauthke") - return true - }).Should(BeTrue(), "getting the secret should succeed eventually and the secret should contain the preauthkey") - - By("Checking that an error is persisted in the headscaleReady condition message") - // replace mock function with original client getter func as this client will fail - grpcClient, err := clientutil.NewHeadscaleGRPCClient(headscaleReconciler.HeadscaleGRPCURL, headscaleReconciler.HeadscaleAPIKey) - Expect(err).ToNot(HaveOccurred(), "There should be no error instantiating the original grpc client") - clusterpkg.ExportSetHeadscaleGRPCClientOnHAR(headscaleReconciler, grpcClient) - // trigger cluster reconcile - Expect(test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: cluster.Name, Namespace: setup.Namespace()}, getCluster)).Should(Succeed(), "There should be no error getting the cluster") - getCluster.SetLabels(map[string]string{"reconcile-me": "true"}) - Expect(test.K8sClient.Update(test.Ctx, getCluster)).Should(Succeed(), "There should be no error updating the cluster") - - Eventually(func(g Gomega) bool { - g.Expect(test.K8sClient.Get(test.Ctx, types.NamespacedName{Name: getCluster.Name, Namespace: setup.Namespace()}, getCluster)).To(Succeed(), "There should be no error getting the cluster") - g.Expect(getCluster.Status.HeadScaleStatus).ToNot(BeNil(), "headscale status should be set") - headscaleCondition := getCluster.Status.GetConditionByType(greenhousev1alpha1.HeadscaleReady) - g.Expect(headscaleCondition).ToNot(BeNil(), "The HeadscaleReady condition should be present") - g.Expect(headscaleCondition.Status).To(Equal(metav1.ConditionFalse), "The HeadscaleReady condition status should be false") - g.Expect(headscaleCondition.Message).To(ContainSubstring("no headscale machine found"), "The client error message should be reflected to the condition") - - // We are testing the part of the status controller depending on the headscale ready condition here! - // All other test setups would expect separation of running controllers - readyCondition := getCluster.Status.GetConditionByType(greenhousev1alpha1.ReadyCondition) - g.Expect(readyCondition).ToNot(BeNil(), "The Ready condition should be present") - g.Expect(readyCondition.Status).To(Equal(metav1.ConditionFalse), "The Ready condition status should be false") - g.Expect(readyCondition.Message).To(ContainSubstring("Headscale connection not ready"), "The default headscale error message should be present") - return true - }). - Should(BeTrue(), "getting the cluster should succeed eventually and status should be set correctly") - - /* - This is commented as the access to the remote cluster requires a https proxy. - Though the proxy is in-place, golang does not account for a proxy on localhost (1) and - injecting custom transport in the client.Client is not supported when using TLS certificates (2). - (1) https://maelvls.dev/go-ignores-proxy-localhost, - (2) https://github.com/kubernetes/client-go/blob/master/transport/transport.go#L38-L40. - - By("Checking the Cluster Status contains the K8s Version in the local cluster") - getCluster = &greenhousev1alpha1.Cluster{} - id = types.NamespacedName{Name: headscaleClusterName, Namespace: orgName} - Eventually(func() bool { - err := test.K8sClient.Get(test.Ctx, id, getCluster) - if err != nil { - return false - } - return getCluster.Status.KubernetesVersion != "" - }, updateTimeout, pollInterval). - Should(BeTrue(), "getting the cluster should succeed eventually and the cluster kubernetes status should be set") - */ - }) -}) diff --git a/pkg/controllers/cluster/status_controller.go b/pkg/controllers/cluster/status_controller.go index 9da93a017..3b406e90a 100644 --- a/pkg/controllers/cluster/status_controller.go +++ b/pkg/controllers/cluster/status_controller.go @@ -72,7 +72,7 @@ func (r *ClusterStatusReconciler) Reconcile(ctx context.Context, req ctrl.Reques allNodesReadyCondition, clusterNodeStatus = r.reconcileNodeStatus(ctx, restClientGetter) } - readyCondition := r.reconcileReadyStatus(cluster, kubeConfigValidCondition) + readyCondition := r.reconcileReadyStatus(kubeConfigValidCondition) conditions = append(conditions, readyCondition, allNodesReadyCondition, kubeConfigValidCondition) @@ -151,19 +151,12 @@ func (r *ClusterStatusReconciler) reconcileClusterSecret( return } -func (r *ClusterStatusReconciler) reconcileReadyStatus(cluster *greenhousev1alpha1.Cluster, kubeConfigValidCondition greenhousev1alpha1.Condition) (readyCondition greenhousev1alpha1.Condition) { +func (r *ClusterStatusReconciler) reconcileReadyStatus(kubeConfigValidCondition greenhousev1alpha1.Condition) (readyCondition greenhousev1alpha1.Condition) { readyCondition = greenhousev1alpha1.UnknownCondition(greenhousev1alpha1.ReadyCondition, "", "") if kubeConfigValidCondition.IsFalse() { readyCondition.Status = metav1.ConditionFalse readyCondition.Message = "kubeconfig not valid - cannot access cluster" - // change ready condition message if headscale not ready - if cluster.Spec.AccessMode == greenhousev1alpha1.ClusterAccessModeHeadscale { - headscaleReadyCondition := cluster.Status.StatusConditions.GetConditionByType(greenhousev1alpha1.HeadscaleReady) - if headscaleReadyCondition == nil || !headscaleReadyCondition.IsTrue() { - readyCondition.Message = "Headscale connection not ready" - } - } } else { readyCondition.Status = metav1.ConditionTrue } diff --git a/pkg/controllers/cluster/suite_test.go b/pkg/controllers/cluster/suite_test.go index bbb7c2057..ccbbe68e9 100644 --- a/pkg/controllers/cluster/suite_test.go +++ b/pkg/controllers/cluster/suite_test.go @@ -4,28 +4,11 @@ package cluster_test import ( - "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" - "net" - "net/http" - "net/http/httputil" - "net/url" - "path/filepath" "testing" "time" - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "google.golang.org/grpc" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/envtest" "github.com/cloudoperators/greenhouse/pkg/admission" clusterpkg "github.com/cloudoperators/greenhouse/pkg/controllers/cluster" @@ -33,9 +16,6 @@ import ( ) var ( - tailscaleProxyURL = "https://127.0.0.1:8080" - - headscaleReconciler *clusterpkg.HeadscaleAccessReconciler bootstrapReconciler *clusterpkg.BootstrapReconciler ) @@ -52,215 +32,15 @@ var _ = BeforeSuite(func() { RemoteClusterBearerTokenValidity: 10 * time.Minute, RenewRemoteClusterBearerTokenAfter: 9 * time.Minute, }).SetupWithManager) - headscaleReconciler = &clusterpkg.HeadscaleAccessReconciler{ - HeadscaleGRPCURL: "willBeMocked", - HeadscaleAPIKey: "willBeMocked", - TailscaleProxy: tailscaleProxyURL, - HeadscalePreAuthenticationKeyMinValidity: 5 * time.Minute, - RemoteClusterBearerTokenValidity: 10 * time.Minute, - RenewRemoteClusterBearerTokenAfter: 9 * time.Minute, - } - test.RegisterController("clusterHeadscaleAccess", (headscaleReconciler).SetupWithManager) + test.RegisterController("clusterStatus", (&clusterpkg.ClusterStatusReconciler{}).SetupWithManager) test.RegisterWebhook("clusterValidation", admission.SetupClusterWebhookWithManager) test.RegisterWebhook("secretsWebhook", admission.SetupSecretWebhookWithManager) test.TestBeforeSuite() - - // inject fake headscale grpc client - clusterpkg.ExportSetHeadscaleGRPCClientOnHAR(headscaleReconciler, newFakeHeadscaleClient()) - - /* - This is commented as the access to the remote cluster requires a https proxy. - Though the proxy is in-place, golang does not account for a proxy on localhost (1) and - injecting custom transport in the client.Client is not supported when using TLS certificates (2). - (1) https://maelvls.dev/go-ignores-proxy-localhost, - (2) https://github.com/kubernetes/client-go/blob/master/transport/transport.go#L38-L40. - go func() { - if err := runReverseProxy(test.Ctx, tailscaleProxyURL, headscaleEnvTest); err != nil { - log.Fatalf("Server error: %v", err) - } - }() - */ }) var _ = AfterSuite(func() { By("tearing down the test environment and remote cluster") test.TestAfterSuite() }) - -func newFakeHeadscaleClientGetter(c client.Client) func(restClientGetter genericclioptions.RESTClientGetter, proxy string, headscaleAddress string) (client.Client, error) { - /* - This is commented as the access to the remote cluster requires a https proxy. - Though the proxy is in-place, golang does not account for a proxy on localhost (1) and - injecting custom transport in the client.Client is not supported when using TLS certificates (2). - (1) https://maelvls.dev/go-ignores-proxy-localhost, - (2) https://github.com/kubernetes/client-go/blob/master/transport/transport.go#L38-L40. - cfgTransportCfg, err := headscaleCfg.TransportConfig() - if err != nil { - return nil, err - } - tlsCfg, err := transport.TLSConfigFor(cfgTransportCfg) - if err != nil { - return nil, err - } - headscaleCfg.Transport = &http.Transport{ - Proxy: func(req *http.Request) (*url.URL, error) { - return url.Parse(tailscaleProxyURL) - }, - DialContext: (&net.Dialer{ - Timeout: 30 * time.Second, - KeepAlive: 30 * time.Second, - DualStack: true, - }).DialContext, - TLSClientConfig: tlsCfg, - ForceAttemptHTTP2: true, - MaxIdleConns: 100, - IdleConnTimeout: 90 * time.Second, - TLSHandshakeTimeout: 10 * time.Second, - ExpectContinueTimeout: 1 * time.Second, - }*/ - - return func(_ genericclioptions.RESTClientGetter, _, _ string) (client.Client, error) { - return c, nil - } -} - -// newFakeHeadscaleClient mocks the Headscale GRPC client and returns the configured responses -func newFakeHeadscaleClient() v1.HeadscaleServiceClient { - cl := test.FakeHeadscaleClient{ - IsUserDeleted: false, - } - cl.GetUserFunc = func(ctx context.Context, in *v1.GetUserRequest, opts ...grpc.CallOption) (*v1.GetUserResponse, error) { - if cl.IsUserDeleted { - return nil, status.Errorf(2, "User not found") - } - return &v1.GetUserResponse{User: &v1.User{ - Id: "1", - Name: in.GetName(), - CreatedAt: timestamppb.Now(), - }}, nil - } - cl.CreateUserFunc = func(ctx context.Context, request *v1.CreateUserRequest, option ...grpc.CallOption) (*v1.CreateUserResponse, error) { - return &v1.CreateUserResponse{User: &v1.User{Id: "1", Name: request.GetName(), CreatedAt: timestamppb.Now()}}, nil - } - cl.ListMachinesFunc = func(ctx context.Context, in *v1.ListMachinesRequest, opts ...grpc.CallOption) (*v1.ListMachinesResponse, error) { - return &v1.ListMachinesResponse{Machines: []*v1.Machine{{ - Id: uint64(1337), - MachineKey: "machine", - NodeKey: "node", - DiscoKey: "disco", - IpAddresses: []string{"127.0.0.1"}, - Name: "myMachine", - User: &v1.User{Id: "1", Name: in.GetUser(), CreatedAt: timestamppb.Now()}, - LastSeen: timestamppb.Now(), - LastSuccessfulUpdate: timestamppb.Now(), - Expiry: timestamppb.New(time.Now().Add(10 * time.Minute)), - PreAuthKey: newFakePreAuthKey(in.GetUser()), - CreatedAt: timestamppb.Now(), - RegisterMethod: v1.RegisterMethod_REGISTER_METHOD_AUTH_KEY, - ForcedTags: []string{"tag:one"}, - InvalidTags: nil, - ValidTags: nil, - GivenName: "myMachine", - Online: true, - }}}, nil - } - cl.CreatePreAuthKeyFunc = func(ctx context.Context, in *v1.CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*v1.CreatePreAuthKeyResponse, error) { - return &v1.CreatePreAuthKeyResponse{ - PreAuthKey: newFakePreAuthKey(in.GetUser()), - }, nil - } - cl.ListPreAuthKeysFunc = func(ctx context.Context, request *v1.ListPreAuthKeysRequest, option ...grpc.CallOption) (*v1.ListPreAuthKeysResponse, error) { - return &v1.ListPreAuthKeysResponse{ - PreAuthKeys: []*v1.PreAuthKey{newFakePreAuthKey(request.GetUser())}, - }, nil - } - cl.DeleteMachineFunc = func(ctx context.Context, in *v1.DeleteMachineRequest, opts ...grpc.CallOption) (*v1.DeleteMachineResponse, error) { - return &v1.DeleteMachineResponse{}, nil - } - cl.DeleteUserFunc = func(ctx context.Context, in *v1.DeleteUserRequest, opts ...grpc.CallOption) (*v1.DeleteUserResponse, error) { - cl.IsUserDeleted = true - return &v1.DeleteUserResponse{}, nil - } - return cl -} - -func newFakePreAuthKey(userName string) *v1.PreAuthKey { - return &v1.PreAuthKey{ - User: userName, - Id: "1", - Key: "someKey", - Reusable: false, - Ephemeral: false, - Used: false, - Expiration: timestamppb.New(time.Now().Add(10 * time.Minute)), - CreatedAt: timestamppb.Now(), - AclTags: nil, - } -} - -//nolint:unused // See comments on unit test with proxy on localhost using TLS certificates. -func runReverseProxy(ctx context.Context, proxyAddress string, testEnv *envtest.Environment) error { - testEnvAPIServerConfig := testEnv.ControlPlane.APIServer - if testEnvAPIServerConfig == nil { - return errors.New("the test environment has no api server configured") - } - remote, err := url.Parse("http://" + net.JoinHostPort(testEnvAPIServerConfig.SecureServing.Address, testEnvAPIServerConfig.SecureServing.Port)) - if err != nil { - return err - } - handler := func(p *httputil.ReverseProxy) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, r *http.Request) { - r.Host = remote.Host - p.ServeHTTP(w, r) - } - } - tlsX509KeyPair, err := tls.LoadX509KeyPair( - filepath.Join(testEnvAPIServerConfig.CertDir, "apiserver.crt"), - filepath.Join(testEnvAPIServerConfig.CertDir, "apiserver.key"), - ) - if err != nil { - return err - } - certPool := x509.NewCertPool() - if !certPool.AppendCertsFromPEM(testEnvAPIServerConfig.CA) { - return errors.New("failed to append CA certs") - } - //nolint:gosec // I promise to not use that in production. - tlsConfig := &tls.Config{ - RootCAs: certPool, - Certificates: []tls.Certificate{tlsX509KeyPair}, - } - proxy := httputil.NewSingleHostReverseProxy(remote) - proxy.Transport = &http.Transport{TLSClientConfig: tlsConfig} - //nolint:forbidigo // I promise to not use that in production. - http.HandleFunc("/", handler(proxy)) - server := &http.Server{TLSConfig: tlsConfig} - proxyURL, err := url.Parse(proxyAddress) - if err != nil { - return err - } - listener, err := net.Listen("tcp4", proxyURL.Host) - if err != nil { - return err - } - defer listener.Close() - tlsListener := tls.NewListener(listener, tlsConfig) - defer tlsListener.Close() - - go func() { - //nolint:gosimple // I promise to not use that in production. - select { - case <-ctx.Done(): - if err := server.Shutdown(ctx); err != nil { - fmt.Printf("error shutting reverse proxy: %v\n", err) - } - } - }() - if err := server.Serve(tlsListener); err != nil { - fmt.Println(err.Error()) - } - <-ctx.Done() - return nil -} diff --git a/pkg/controllers/cluster/util.go b/pkg/controllers/cluster/util.go index 6a79317bc..bbc98cca2 100644 --- a/pkg/controllers/cluster/util.go +++ b/pkg/controllers/cluster/util.go @@ -6,23 +6,15 @@ package cluster import ( "context" "fmt" - "strings" "time" - "google.golang.org/protobuf/types/known/timestamppb" - - headscalev1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/pkg/errors" - "google.golang.org/grpc/status" authenticationv1 "k8s.io/api/authentication/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" - "k8s.io/client-go/tools/record" "k8s.io/utils/ptr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -159,7 +151,6 @@ func reconcileClusterRoleBindingInRemoteCluster(ctx context.Context, k8sClient c type tokenHelper struct { client.Client Proxy string - HeadscaleAddress string RemoteClusterBearerTokenValidity time.Duration RenewRemoteClusterBearerTokenAfter time.Duration } @@ -201,11 +192,6 @@ func (t *tokenHelper) ReconcileServiceAccountToken(ctx context.Context, restClie if err != nil { return err } - case greenhousev1alpha1.ClusterAccessModeHeadscale: - generatedKubeConfig, err = generateNewClientKubeConfigHeadscale(ctx, restClientGetter, tokenRequestResponse.Status.Token, cluster, t.Proxy, t.HeadscaleAddress) - if err != nil { - return err - } default: return fmt.Errorf("unknown access mode %s", cluster.Spec.AccessMode) } @@ -239,29 +225,6 @@ func (t *tokenHelper) ReconcileServiceAccountToken(ctx context.Context, restClie return err } -// generateNewClientKubeConfigHeadscale generates a kubeconfig for the client to access the cluster from REST config coming from the secret plus the modifications needed for headscale. -func generateNewClientKubeConfigHeadscale(_ context.Context, restConfigGetter *clientutil.RestClientGetter, bearerToken string, cluster *greenhousev1alpha1.Cluster, proxy, headscaleAddress string) ([]byte, error) { - restConfig, err := restConfigGetter.ToRawKubeConfigLoader().ClientConfig() - if err != nil { - return nil, errors.Wrapf(err, "failed to load kube clientConfig for cluster %s", cluster.GetName()) - } - - kubeConfigGenerator := &KubeConfigHelper{ - Host: "https://" + headscaleAddress, - TLSServerName: "127.0.0.1", - ProxyURL: proxy, - CAData: restConfig.CAData, - BearerToken: bearerToken, - Username: serviceAccountName, - Namespace: cluster.GetNamespace(), - } - kubeconfigByte, err := clientcmd.Write(kubeConfigGenerator.RestConfigToAPIConfig(cluster.Name)) - if err != nil { - return nil, errors.Wrapf(err, "failed to generate kubeconfig for cluster %s", cluster.GetName()) - } - return kubeconfigByte, nil -} - // reconcileRemoteAPIServerVersion fetches the api server version from the remote cluster and reflects it in the cluster CR func reconcileRemoteAPIServerVersion(ctx context.Context, restConfigGetter *clientutil.RestClientGetter, k8sclient client.Client, cluster *greenhousev1alpha1.Cluster) error { k8sVersion, err := clientutil.GetKubernetesVersion(restConfigGetter) @@ -284,67 +247,3 @@ func deleteNamespaceInRemoteCluster(ctx context.Context, remoteK8sClient client. // Ignore errors if was already deleted. return client.IgnoreNotFound(err) } - -// ReconcileHeadscaleUser ensure a user for the cluster exists in the headscale coordination server. -func ReconcileHeadscaleUser(ctx context.Context, recorder record.EventRecorder, cluster *greenhousev1alpha1.Cluster, headscaleGRPCClient headscalev1.HeadscaleServiceClient) error { - createResp, err := headscaleGRPCClient.CreateUser(ctx, &headscalev1.CreateUserRequest{ - Name: headscaleKeyForCluster(cluster), - }) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return err - } - switch { - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to create user %s", headscaleKeyForCluster(cluster)) - case strings.Contains(errStatus.Message(), "already exists"): - return nil - } - return err - } - recorder.Eventf(cluster, corev1.EventTypeNormal, "HeadscaleUserCreated", "Headscale user %s created for cluster", createResp.User.Name) - return nil -} - -// ReconcilePreAuthorizationKey ensure a pre-authorization key exists for the given cluster. -func ReconcilePreAuthorizationKey(ctx context.Context, cluster *greenhousev1alpha1.Cluster, headscaleGRPCClient headscalev1.HeadscaleServiceClient, headscalePreAuthenticationKeyMinValidity time.Duration) (*headscalev1.PreAuthKey, error) { - // Check whether an existing pre-authorization key can be used. - resp, err := headscaleGRPCClient.ListPreAuthKeys(ctx, &headscalev1.ListPreAuthKeysRequest{ - User: headscaleKeyForCluster(cluster), - }) - if err != nil { - return nil, err - } - for _, key := range resp.GetPreAuthKeys() { - if isPreAuthenticationKeyIsNotExpired(key, headscalePreAuthenticationKeyMinValidity) { - return key, nil - } - } - - // Request a new pre-authorization key. - expiration := time.Now().UTC().Add(7 * 24 * time.Hour) - createPreAuth := &headscalev1.CreatePreAuthKeyRequest{ - User: headscaleKeyForCluster(cluster), - Reusable: true, - Ephemeral: true, - Expiration: timestamppb.New(expiration), - AclTags: []string{"tag:greenhouse", "tag:client", "tag:" + headscaleKeyForCluster(cluster)}, - } - createResp, err := headscaleGRPCClient.CreatePreAuthKey(ctx, createPreAuth) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return nil, err - } - switch { - case strings.Contains(errStatus.Message(), "Unauthorized"): - return nil, fmt.Errorf("headscale: unauthorized to create user %s", headscaleKeyForCluster(cluster)) - case strings.Contains(errStatus.Message(), "tag is invalid"): - return nil, fmt.Errorf("headscale: failed to create PreAuthKey for user %s: %s", headscaleKeyForCluster(cluster), errStatus.Message()) - } - return nil, errors.Wrapf(err, "failed to create PreAuthenticationKey for user %s", headscaleKeyForCluster(cluster)) - } - log.FromContext(ctx).Info("PreAuthenticationKey issued", "user", headscaleKeyForCluster(cluster), "expireDate", createResp.PreAuthKey.Expiration.AsTime()) - return createResp.PreAuthKey, nil -} diff --git a/pkg/controllers/fixtures/cluster.yaml b/pkg/controllers/fixtures/cluster.yaml index e5412f496..ecc69b56d 100644 --- a/pkg/controllers/fixtures/cluster.yaml +++ b/pkg/controllers/fixtures/cluster.yaml @@ -50,7 +50,6 @@ spec: the Greenhouse operator. enum: - direct - - headscale type: string required: - accessMode @@ -63,53 +62,6 @@ spec: timestamp of the bearer token used to access the cluster. format: date-time type: string - headScaleStatus: - description: HeadScaleStatus contains the current status of the headscale - client. - properties: - createdAt: - format: date-time - type: string - expiry: - format: date-time - type: string - forcedTags: - items: - type: string - type: array - id: - format: int64 - type: integer - ipAddresses: - items: - type: string - type: array - name: - type: string - online: - type: boolean - preAuthKey: - description: PreAuthKey reflects the status of the pre-authentication - key used by the Headscale machine. - properties: - createdAt: - format: date-time - type: string - ephemeral: - type: boolean - expiration: - format: date-time - type: string - id: - type: string - reusable: - type: boolean - used: - type: boolean - user: - type: string - type: object - type: object kubernetesVersion: description: KubernetesVersion reflects the detected Kubernetes version of the cluster. diff --git a/pkg/controllers/fixtures/cluster_update.yaml b/pkg/controllers/fixtures/cluster_update.yaml index a004ccec1..0e5ebd944 100644 --- a/pkg/controllers/fixtures/cluster_update.yaml +++ b/pkg/controllers/fixtures/cluster_update.yaml @@ -50,7 +50,6 @@ spec: the Greenhouse operator. enum: - direct - - headscale type: string newRequiredField: type: string @@ -67,53 +66,6 @@ spec: timestamp of the bearer token used to access the cluster. format: date-time type: string - headScaleStatus: - description: HeadScaleStatus contains the current status of the headscale - client. - properties: - createdAt: - format: date-time - type: string - expiry: - format: date-time - type: string - forcedTags: - items: - type: string - type: array - id: - format: int64 - type: integer - ipAddresses: - items: - type: string - type: array - name: - type: string - online: - type: boolean - preAuthKey: - description: PreAuthKey reflects the status of the pre-authentication - key used by the Headscale machine. - properties: - createdAt: - format: date-time - type: string - ephemeral: - type: boolean - expiration: - format: date-time - type: string - id: - type: string - reusable: - type: boolean - used: - type: boolean - user: - type: string - type: object - type: object kubernetesVersion: description: KubernetesVersion reflects the detected Kubernetes version of the cluster. diff --git a/pkg/headscalectl/apikey.go b/pkg/headscalectl/apikey.go deleted file mode 100644 index a0f4864b8..000000000 --- a/pkg/headscalectl/apikey.go +++ /dev/null @@ -1,256 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package headscalectl - -import ( - "context" - "errors" - "fmt" - "strings" - "time" - - headscalev1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/prometheus/common/model" - "github.com/spf13/cobra" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - defaultAPIKeyExpiry = "90d" - apiPrefixLength = 10 -) - -var ( - apiKeyExpiry string - socketPath string - socketCall bool - prefix string - secretName string - secretNamespace string -) - -func init() { - rootCmd.AddCommand(apiKeyCMD) - apiKeyCMD.AddCommand(createAPIKeyCmd()) - apiKeyCMD.AddCommand(listAPIKeyCmd()) - apiKeyCMD.AddCommand(expireAPIKeyCmd()) -} - -var apiKeyCMD = &cobra.Command{ - Use: "apikey", - Short: "Commands to interact with Headscale API keys", -} - -var createAPIKeyCmdUsage = "create" - -type createAPIKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func createAPIKeyCmd() *cobra.Command { - c := createAPIKeyCmdOptions{} - cmd := &cobra.Command{ - Use: createAPIKeyCmdUsage, - Short: "Creates a new APIKey for Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - var err error - var grpcClient headscalev1.HeadscaleServiceClient - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - if !socketCall { - grpcClient, err = headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - } else { - grpcClient, err = headscaleGRPCSocketClientFunc(socketPath) - if err != nil { - return err - } - } - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - cmd.Flags().StringVarP(&apiKeyExpiry, "expiration", "e", defaultAPIKeyExpiry, "Expiration time for the apikey") - cmd.Flags().StringVar(&socketPath, "socket-path", "", "Path to the headscale agent socket") - cmd.Flags().BoolVar(&socketCall, "socket", false, "Call the headscale agent socket") - cmd.Flags().StringVar(&secretName, "secret-name", "", "Name of the secret to create") - cmd.Flags().StringVar(&secretNamespace, "secret-namespace", "", "Kubernetes namespace to create the secret in") - return cmd -} - -func (o *createAPIKeyCmdOptions) run() error { - duration, err := model.ParseDuration(apiKeyExpiry) - if err != nil { - log.FromContext(context.Background()).Error(err, "error parsing duration") - return err - } - timeDuration := time.Duration(duration) - if timeDuration < time.Hour*24 { - log.FromContext(context.Background()).Info("duration must be greater than 1 day") - timeDuration = time.Hour * 24 - } - - createResp, err := o.headscaleGRPCClient.CreateApiKey(context.Background(), &headscalev1.CreateApiKeyRequest{ - Expiration: timestamppb.New(time.Now().Add(timeDuration)), - }) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return errors.New("headscale: unauthorized to create APIKey") - default: - return err - } - } - if o.outputFormat == "secret" { - tickerDuration := timeDuration - (time.Hour * 8) - ticker := time.NewTicker(tickerDuration) - for ; true; <-ticker.C { - createOrUpdateSecretInCluster(createResp.ApiKey, secretName, secretNamespace) - log.FromContext(context.Background()).Info("New APIKey would be generated", "at:", time.Now().Add(tickerDuration)) - } - } - Output(createResp.ApiKey, createResp.ApiKey, o.outputFormat) - return nil -} - -var listAPIKeyCmdUsage = "list" - -type listAPIKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func listAPIKeyCmd() *cobra.Command { - c := listAPIKeyCmdOptions{} - cmd := &cobra.Command{ - Use: listAPIKeyCmdUsage, - Short: "Lists all APIKey for Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - var err error - var grpcClient headscalev1.HeadscaleServiceClient - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - if !socketCall { - grpcClient, err = headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - } else { - grpcClient, err = headscaleGRPCSocketClientFunc(socketPath) - } - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - cmd.Flags().StringVar(&socketPath, "socket-path", "", "Path to the headscale agent socket") - cmd.Flags().BoolVar(&socketCall, "socket", false, "Call the headscale agent socket") - return cmd -} - -func (o *listAPIKeyCmdOptions) run() error { - listResp, err := o.headscaleGRPCClient.ListApiKeys(context.Background(), &headscalev1.ListApiKeysRequest{}) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return errors.New("headscale: unauthorized to list APIKey") - default: - return err - } - } - if o.outputFormat != "" { - Output(listResp.ApiKeys, "", o.outputFormat) - return nil - } - log.FromContext(context.Background()).Info("APIKey", "apikey", listResp.ApiKeys) - return nil -} - -var expireAPIKeyCmdUsage = "expire" - -type expireAPIKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func expireAPIKeyCmd() *cobra.Command { - c := expireAPIKeyCmdOptions{} - cmd := &cobra.Command{ - Use: expireAPIKeyCmdUsage, - Short: "Expire an APIKey for Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - var err error - var grpcClient headscalev1.HeadscaleServiceClient - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - if !socketCall { - grpcClient, err = headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - } else { - grpcClient, err = headscaleGRPCSocketClientFunc(socketPath) - } - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - - cmd.Flags().StringVar(&prefix, "prefix", "", "Prefix of the apikey to expire") - cmd.Flags().StringVar(&socketPath, "socket-path", "", "Path to the headscale agent socket") - cmd.Flags().BoolVar(&socketCall, "socket", false, "Call the headscale agent socket") - return cmd -} - -func (o *expireAPIKeyCmdOptions) run() error { - if len(prefix) != apiPrefixLength { - return fmt.Errorf("prefix must be exactly %d characters long", apiPrefixLength) - } - expResp, err := o.headscaleGRPCClient.ExpireApiKey(context.Background(), &headscalev1.ExpireApiKeyRequest{ - Prefix: prefix, - }) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return errors.New("headscale: unauthorized to expire APIKey") - case strings.Contains(errStatus.Message(), "record not found"): - return errors.New("headscale: APIKey not found") - default: - return err - } - } - Output(expResp, "APIKey expired", o.outputFormat) - return nil -} diff --git a/pkg/headscalectl/preauthkey.go b/pkg/headscalectl/preauthkey.go deleted file mode 100644 index e355e0844..000000000 --- a/pkg/headscalectl/preauthkey.go +++ /dev/null @@ -1,261 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package headscalectl - -import ( - "context" - "errors" - "fmt" - "os" - "strings" - "time" - - headscalev1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/prometheus/common/model" - "github.com/spf13/cobra" - "google.golang.org/grpc/status" - "google.golang.org/protobuf/types/known/timestamppb" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -const ( - DefaultPreAuthKeyExpiry = "1d" -) - -var ( - reusable bool - ephemeral bool - durationStr string - tags []string - user string - key string - filePath string - force bool -) - -func init() { - rootCmd.AddCommand(preauthKeyCmd) - preauthKeyCmd.AddCommand(createPreAuthKeyCmd()) - preauthKeyCmd.AddCommand(listPreAuthKeyCmd()) - preauthKeyCmd.AddCommand(expirePreAuthKeyCmd()) - createPreAuthKeyCmd().DisableSuggestions = true -} - -var preauthKeyCmd = &cobra.Command{ - Use: "preauthkey", - Short: "Command to issue PreAuthKey", -} - -var createPreAuthKeyCmdUsage = "create [flags] [username]" - -type createPreAuthKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func createPreAuthKeyCmd() *cobra.Command { - c := createPreAuthKeyCmdOptions{} - cmd := &cobra.Command{ - Use: createPreAuthKeyCmdUsage, - Short: "Create a preauthkey for a headscale user", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - cmd.Flags().BoolVar(&reusable, "reusable", false, "Create a reusable preauthkey") - cmd.Flags().BoolVar(&ephemeral, "ephemeral", false, "Create an ephemeral preauthkey") - cmd.Flags().StringVar(&durationStr, "expiration", DefaultPreAuthKeyExpiry, "Expiration time for the preauthkey") - cmd.Flags().StringSliceVar(&tags, "tags", []string{}, "Tags for the preauthkey") - cmd.Flags().StringVarP(&user, "user", "u", "", "User for the preauthkey") - cmd.Flags().BoolVar(&force, "force", false, "Force the creation of a preauthkey, by creating the user if it doesn't exist") - cmd.Flags().StringVar(&filePath, "file", "", "File to write the preauthkey to") - return cmd -} - -func (o *createPreAuthKeyCmdOptions) run() error { - if user == "" { - return errors.New("user is required to create preauthkeys") - } - duration, err := model.ParseDuration(durationStr) - if err != nil { - log.FromContext(context.Background()).Error(err, "error parsing duration") - return err - } - - createResp, err := o.headscaleGRPCClient.CreatePreAuthKey(context.Background(), &headscalev1.CreatePreAuthKeyRequest{ - User: user, - Reusable: reusable, - Ephemeral: ephemeral, - Expiration: timestamppb.New(time.Now().Add(time.Duration(duration))), - AclTags: tags, - }) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to create preauthkey for user %s", user) - case strings.Contains(errStatus.Message(), "User not found"): - if force { - createResp, err := o.headscaleGRPCClient.CreateUser(context.Background(), &headscalev1.CreateUserRequest{ - Name: user, - }) - if err != nil { - return err - } - Output(createResp.User, "", o.outputFormat) - return o.run() - } - return fmt.Errorf("headscale: user %s not found", user) - default: - return err - } - } - - if filePath != "" { - err := os.WriteFile(filePath, []byte(createResp.PreAuthKey.Key), 0644) - if err != nil { - log.FromContext(context.Background()).Error(err, "error writing preauthkey to file") - return err - } - fmt.Println("PreAuthKey written to file", filePath) - return nil - } - Output(createResp.PreAuthKey, createResp.PreAuthKey.Key, o.outputFormat) - - return nil -} - -var listPreAuthKeyCmdUsage = "list [flags] [username]" - -type listPreAuthKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func listPreAuthKeyCmd() *cobra.Command { - c := listPreAuthKeyCmdOptions{} - cmd := &cobra.Command{ - Use: listPreAuthKeyCmdUsage, - Short: "List a preauthkey for a headscale user", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - cmd.Flags().StringVarP(&user, "user", "u", "", "User for the preauthkey") - return cmd -} - -func (o *listPreAuthKeyCmdOptions) run() error { - if user == "" { - return errors.New("user is required to list preauthkeys") - } - listResp, err := o.headscaleGRPCClient.ListPreAuthKeys(context.Background(), &headscalev1.ListPreAuthKeysRequest{ - User: user, - }) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to list preauthkey for user %s", user) - default: - return err - } - } - if o.outputFormat != "" { - Output(listResp.PreAuthKeys, "", o.outputFormat) - return nil - } - log.FromContext(context.Background()).Info("PreAuthKey", "user", listResp.PreAuthKeys) - return nil -} - -var expirePreAuthKeyCmdUsage = "expire [flags] [username]" - -type expirePreAuthKeyCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func expirePreAuthKeyCmd() *cobra.Command { - c := expirePreAuthKeyCmdOptions{} - cmd := &cobra.Command{ - Use: expirePreAuthKeyCmdUsage, - Short: "Expire a preauthkey for a headscale user", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - } - cmd.Flags().StringVarP(&user, "user", "u", "", "User for the preauthkey") - cmd.Flags().StringVar(&key, "key", "", "preauthkey") - - return cmd -} - -func (o *expirePreAuthKeyCmdOptions) run() error { - if key == "" || user == "" { - return errors.New("user and key are required to expire preauthkeys") - } - delResp, err := o.headscaleGRPCClient.ExpirePreAuthKey(context.Background(), &headscalev1.ExpirePreAuthKeyRequest{ - User: user, - Key: key, - }) - if err != nil { - errStatus, ok := status.FromError(err) - switch { - case !ok: - return err - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to list preauthkey for user %s", user) - case strings.Contains(errStatus.Message(), "AuthKey expired"): - return fmt.Errorf("headscale: preauthkey %s for user %s already expired", key, user) - default: - return err - } - } - Output(delResp, "PreAuthKey expired", o.outputFormat) - return nil -} diff --git a/pkg/headscalectl/root.go b/pkg/headscalectl/root.go deleted file mode 100644 index e19e492d1..000000000 --- a/pkg/headscalectl/root.go +++ /dev/null @@ -1,52 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package headscalectl - -import ( - "github.com/spf13/cobra" - "go.uber.org/zap/zapcore" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - - "github.com/cloudoperators/greenhouse/pkg/clientutil" - "github.com/cloudoperators/greenhouse/pkg/version" -) - -const programName = "headscalectl" - -var ( - headscaleGRPCURL string - headscaleAPIKey string -) - -func init() { - rootCmd.PersistentFlags().StringVarP(&headscaleGRPCURL, "headscale-cli-address", "a", clientutil.GetEnvOrDefault("HEADSCALE_CLI_ADDRESS", ""), "Headscale API address. Can be set via HEADSCALE_CLI_ADDRESS env variable. Only GRPC is supported.") - rootCmd.PersistentFlags().StringVarP(&headscaleAPIKey, "headscale-api-key", "k", clientutil.GetEnvOrDefault("HEADSCALE_CLI_API_KEY", ""), "Headscale API key. Can be set via HEADSCALE_CLI_API_KEY env variable") - rootCmd.PersistentFlags().StringP("output", "o", "", "Output format. Empty for human-readable, 'json','json-line', 'yaml' or 'secret' | secret only works for apikey create") - rootCmd.DisableSuggestions = true - - opts := zap.Options{ - Development: true, - TimeEncoder: zapcore.RFC3339TimeEncoder, - } - - ctrl.SetLogger(zap.New( - zap.UseFlagOptions(&opts)), - ) - rootCmd.DisableSuggestions = true -} - -// rootCmd for headscalectl. -var rootCmd = &cobra.Command{ - Use: programName, - Short: "Headscale command line tool for REST API", - Version: version.GetVersionTemplate(programName), -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - log.FromContext(ctrl.SetupSignalHandler()).Error(err, "Error executing command") - } -} diff --git a/pkg/headscalectl/user.go b/pkg/headscalectl/user.go deleted file mode 100644 index 03a29069a..000000000 --- a/pkg/headscalectl/user.go +++ /dev/null @@ -1,254 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package headscalectl - -import ( - "context" - "errors" - "fmt" - "strings" - - headscalev1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "github.com/spf13/cobra" - "google.golang.org/grpc/status" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -func init() { - rootCmd.AddCommand(userCmd) - userCmd.AddCommand(createUserCmd()) - userCmd.AddCommand(deleteUserCmd()) - userCmd.AddCommand(getUserCmd()) - userCmd.AddCommand(listUserCmd()) -} - -var userCmd = &cobra.Command{ - Use: "user", - Short: "Commands to interact with Users", -} - -var createUserCmdUsage = "create [username]" - -type createUserCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func createUserCmd() *cobra.Command { - c := createUserCmdOptions{} - return &cobra.Command{ - Use: createUserCmdUsage, - Short: "Create a User in Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - userName := args[0] - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run(userName) - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - Args: cobra.ExactArgs(1), - } -} - -func (o *createUserCmdOptions) run(userName string) error { - createResp, err := o.headscaleGRPCClient.CreateUser(context.Background(), &headscalev1.CreateUserRequest{ - Name: userName, - }) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return err - } - switch { - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to create user %s", userName) - case strings.Contains(errStatus.Message(), "already exists"): - log.FromContext(context.Background()).Info("User already exists", "user", userName) - return nil - } - return err - } - - Output(createResp.User, "User created", o.outputFormat) - return nil -} - -var deleteUserCmdUsage = "delete [username]" - -type deleteUserCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func deleteUserCmd() *cobra.Command { - c := deleteUserCmdOptions{} - return &cobra.Command{ - Use: deleteUserCmdUsage, - Short: "Delete a User in Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - userName := args[0] - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run(userName) - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - Args: cobra.ExactArgs(1), - } -} - -func (o *deleteUserCmdOptions) run(userName string) error { - delResp, err := o.headscaleGRPCClient.DeleteUser(context.Background(), &headscalev1.DeleteUserRequest{ - Name: userName, - }) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return err - } - switch { - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to delete user %s", userName) - case strings.Contains(errStatus.Message(), "not found"): - return fmt.Errorf("headscale: user %s not found", userName) - } - return err - } - Output(delResp, "User deleted", o.outputFormat) - return nil -} - -var getUserCmdUsage = "get [username]" - -type getUserCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func getUserCmd() *cobra.Command { - c := getUserCmdOptions{} - return &cobra.Command{ - Use: getUserCmdUsage, - Short: "Get a User in Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - userName := args[0] - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run(userName) - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - Args: cobra.ExactArgs(1), - } -} - -func (o *getUserCmdOptions) run(userName string) error { - getResp, err := o.headscaleGRPCClient.GetUser(context.Background(), &headscalev1.GetUserRequest{ - Name: userName, - }) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return err - } - switch { - case strings.Contains(errStatus.Message(), "Unauthorized"): - return fmt.Errorf("headscale: unauthorized to get user %s", userName) - case strings.Contains(errStatus.Message(), "not found"): - return fmt.Errorf("headscale: user %s not found", userName) - } - return err - } - if o.outputFormat != "" { - Output(getResp.User, "", o.outputFormat) - return nil - } - log.FromContext(context.Background()).Info("User", "user", getResp.User.Name, "created at", getResp.User.CreatedAt) - return nil -} - -var listUserCmdUsage = "list" - -type listUserCmdOptions struct { - headscaleGRPCClient headscalev1.HeadscaleServiceClient - outputFormat string -} - -func listUserCmd() *cobra.Command { - c := listUserCmdOptions{} - return &cobra.Command{ - Use: listUserCmdUsage, - Short: "List Users in Headscale", - RunE: func(cmd *cobra.Command, args []string) error { - if o, err := cmd.Flags().GetString("output"); err != nil { - return fmt.Errorf("invalid value for flag --output: %w", err) - } else { - c.outputFormat = o - } - grpcClient, err := headscaleGRCPClientFunc(headscaleGRPCURL, headscaleAPIKey) - if err != nil { - return err - } - c.headscaleGRPCClient = grpcClient - return c.run() - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return validateFlags() - }, - Args: cobra.NoArgs, - } -} - -func (o *listUserCmdOptions) run() error { - listResp, err := o.headscaleGRPCClient.ListUsers(context.Background(), &headscalev1.ListUsersRequest{}) - if err != nil { - errStatus, ok := status.FromError(err) - if !ok { - return err - } - - if strings.Contains(errStatus.Message(), "Unauthorized") { - return errors.New("headscale: unauthorized to list users") - } - return err - } - if o.outputFormat != "" { - Output(listResp.Users, "", o.outputFormat) - return nil - } - var userNames []string - for _, user := range listResp.Users { - userNames = append(userNames, user.Name) - } - log.FromContext(context.Background()).Info("Users", "users", userNames) - return nil -} diff --git a/pkg/headscalectl/util.go b/pkg/headscalectl/util.go deleted file mode 100644 index b76b8999b..000000000 --- a/pkg/headscalectl/util.go +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package headscalectl - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "os" - - "gopkg.in/yaml.v3" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/rest" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/client/config" - "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/cloudoperators/greenhouse/pkg/clientutil" -) - -const ( - secretKey = "HEADSCALE_CLI_API_KEY" //nolint:gosec -) - -var ( - headscaleGRCPClientFunc = clientutil.NewHeadscaleGRPCClient - headscaleGRPCSocketClientFunc = clientutil.NewHeadscaleGRPCSocketClient -) - -func validateFlags() error { - if socketCall { - if socketPath == "" { - return errors.New("socket path is empty") - } - return nil - } - if headscaleGRPCURL == "" { - return errors.New("headscale GRPC URL is empty") - } - if headscaleAPIKey == "" { - return errors.New("headscale API key is empty") - } - return nil -} - -func getKubeconfigOrDie(kubecontext string) *rest.Config { - if kubecontext == "" { - kubecontext = os.Getenv("KUBECONTEXT") - } - restConfig, err := config.GetConfigWithContext(kubecontext) - if err != nil { - log.FromContext(context.Background()).Error(err, "Failed to load kubeconfig") - os.Exit(1) - } - return restConfig -} - -/* - func checkIfSecretExistsInCluster(secretName, secretNamespace string) (*corev1.Secret, bool) { - var kubeClient client.Client - restConfig := getKubeconfigOrDie("") - kubeClient, err := clientutil.NewK8sClient(restConfig) - if err != nil { - log.FromContext(context.Background()).Error(err, "Failed to create Kubernetes client") - } - secret := new(corev1.Secret) - secret.Name = secretName - secret.Namespace = secretNamespace - err = kubeClient.Get(context.Background(), client.ObjectKey{ - Name: secret.Name, - Namespace: secret.Namespace, - }, secret) - if err != nil { - return nil, false - } - return secret, true - } -*/ -func createOrUpdateSecretInCluster(apiKey, secretName, secretNamespace string) { - var kubeClient client.Client - restConfig := getKubeconfigOrDie("") - kubeClient, err := clientutil.NewK8sClient(restConfig) - if err != nil { - log.FromContext(context.Background()).Error(err, "Failed to create Kubernetes client") - } - secret := new(corev1.Secret) - secret.Name = secretName - secret.Namespace = secretNamespace - result, err := clientutil.CreateOrPatch(context.Background(), kubeClient, secret, func() error { - secret.StringData = map[string]string{ - secretKey: apiKey, - } - return nil - }) - if err != nil { - log.FromContext(context.Background()).Error(err, "Failed to create secret") - } - switch result { - case clientutil.OperationResultCreated: - log.FromContext(context.Background()).Info("created secret", "name", secret.Name) - case clientutil.OperationResultUpdated: - log.FromContext(context.Background()).Info("updated secret", "name", secret.Name) - } -} - -func Output(result interface{}, override, outputFormat string) { - var jsonBytes []byte - var err error - switch outputFormat { - case "json": - jsonBytes, err = json.MarshalIndent(result, "", "\t") - if err != nil { - log.FromContext(context.Background()).Error(err, "Error marshalling JSON") - } - case "json-line": - jsonBytes, err = json.Marshal(result) - if err != nil { - log.FromContext(context.Background()).Error(err, "Error marshalling JSON") - } - case "yaml": - jsonBytes, err = yaml.Marshal(result) - if err != nil { - log.FromContext(context.Background()).Error(err, "Error marshalling YAML") - } - default: - log.FromContext(context.Background()).Info(override) - return - } - fmt.Println(string(jsonBytes)) -} diff --git a/pkg/tailscale-starter/root.go b/pkg/tailscale-starter/root.go deleted file mode 100644 index 6427beb5a..000000000 --- a/pkg/tailscale-starter/root.go +++ /dev/null @@ -1,90 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package tailscalestarter - -import ( - "errors" - "net/http" - "os" - "os/exec" - "time" - - "github.com/spf13/cobra" - "go.uber.org/zap/zapcore" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" - "tailscale.com/client/tailscale" - - "github.com/cloudoperators/greenhouse/pkg/version" -) - -const programName = "tailscale_starter" - -var ( - localClient tailscale.LocalClient -) - -func init() { - opts := zap.Options{ - Development: true, - TimeEncoder: zapcore.RFC3339TimeEncoder, - } - ctrl.SetLogger(zap.New( - zap.UseFlagOptions(&opts)), - ) -} - -// rootCmd for headscalectl. -func rootCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: programName, - Short: "tailscale_starter is a tool to start tailscale and expose a health endpoint", - Version: version.GetVersionTemplate(programName), - RunE: func(cmd *cobra.Command, args []string) error { - if len(args) > 0 { - return errors.New("unexpected non-flag arguments to 'tailscale status'") - } - - go func() { - // start tailscale - cmd := exec.Command("/tailscale/run.sh") - cmd.Stdout = os.Stdout // or any other io.Writer - cmd.Stderr = os.Stdout // or any other io.Writer - if err := cmd.Run(); err != nil { - log.FromContext(ctrl.SetupSignalHandler()).Error(err, "Error starting tailscaled") - os.Exit(1) - } - }() - - httpServer := &http.Server{ - Addr: ":8090", - Handler: newHealthMux(), - IdleTimeout: 30 * time.Second, - ReadHeaderTimeout: 20 * time.Second, - ReadTimeout: 20 * time.Second, - } - - if err := httpServer.ListenAndServe(); err != nil { - log.FromContext(ctrl.SetupSignalHandler()).Error(err, "Error starting HTTP server") - os.Exit(1) - } - - return nil - }, - PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - return setPreAuthKey() - }, - } - cmd.DisableSuggestions = true - cmd.Flags().StringVar(&localClient.Socket, "socket", "/var/run/tailscale/tailscaled.sock", "Path to the tailscale socket") - return cmd -} - -func Execute() { - if err := rootCmd().Execute(); err != nil { - log.FromContext(ctrl.SetupSignalHandler()).Error(err, "Error executing command") - os.Exit(1) - } -} diff --git a/pkg/tailscale-starter/util.go b/pkg/tailscale-starter/util.go deleted file mode 100644 index c22b81ae5..000000000 --- a/pkg/tailscale-starter/util.go +++ /dev/null @@ -1,105 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package tailscalestarter - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "os" - - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/log" - - "tailscale.com/ipn" - "tailscale.com/ipn/ipnstate" -) - -func fileExists(fileName string) bool { - fileInfo, err := os.Stat(fileName) - if os.IsNotExist(err) { - return false - } - return !fileInfo.IsDir() -} - -func readFile(fileName string) string { - buff, err := os.ReadFile(fileName) - if err != nil { - return "" - } - return string(buff) -} - -func setPreAuthKey() error { - switch { - case os.Getenv("TS_AUTHKEY") != "", os.Getenv("TS_AUTH_KEY") != "": - log.FromContext(ctrl.SetupSignalHandler()).Info("TS_AUTHKEY or TS_AUTH_KEY is set, skipping preauthkey") - return nil - case fileExists("/preauthkey/key"): - if err := os.Setenv("TS_AUTHKEY", readFile("/preauthkey/key")); err != nil { - return err - } - return nil - default: - return errors.New("no preauthkey found, stopping tailscale") - } -} - -func isRunningOrStarting(status *ipnstate.Status) (description string, ok bool) { - switch status.BackendState { - case ipn.Stopped.String(): - return "Tailscale is stopped.", false - case ipn.NeedsLogin.String(): - return "Logged out.", false - case ipn.NeedsMachineAuth.String(): - return "Client needs to be approved", false - case ipn.Running.String(), ipn.Starting.String(): - return status.BackendState, true - default: - return "unknown state: " + status.BackendState, false - } -} - -type healthResponse struct { - TailscaleVersion *string `json:"tailscaleVersion,omitempty"` - ErrorMessage *string `json:"errorMessage,omitempty"` - HealthStatus []string `json:"healthStatus,omitempty"` -} - -func healthHandler(w http.ResponseWriter, _ *http.Request) { - response := new(healthResponse) - w.Header().Set("Content-Type", "application/json") - localClient.UseSocketOnly = true - status, err := localClient.StatusWithoutPeers(context.Background()) - if err != nil { - w.WriteHeader(http.StatusInternalServerError) - response.HealthStatus = append(response.HealthStatus, err.Error()) - } - description, ok := isRunningOrStarting(status) - if ok { - w.WriteHeader(http.StatusOK) - response.TailscaleVersion = &status.Version - response.HealthStatus = append(response.HealthStatus, description) - } else { - w.WriteHeader(http.StatusInternalServerError) - if len(status.Health) > 0 && (status.BackendState == ipn.Starting.String() || status.BackendState == ipn.NoState.String()) { - response.HealthStatus = append(response.HealthStatus, status.Health...) - } - response.TailscaleVersion = &status.Version - response.HealthStatus = append(response.HealthStatus, description) - } - jsonResp, _ := json.Marshal(response) - _, err = w.Write(jsonResp) - if err != nil { - log.FromContext(ctrl.SetupSignalHandler()).Error(err, "error during json Marshal") - } -} - -func newHealthMux() *http.ServeMux { - mux := http.NewServeMux() - mux.HandleFunc("/healthz", healthHandler) - return mux -} diff --git a/pkg/tcp-proxy/metrics/metrics.go b/pkg/tcp-proxy/metrics/metrics.go deleted file mode 100644 index 956b251b5..000000000 --- a/pkg/tcp-proxy/metrics/metrics.go +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package metrics - -import ( - "errors" - "net/http" - "os" - "sync/atomic" - - "github.com/google/uuid" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "k8s.io/klog/v2" -) - -type Helper struct { - metricsServer *http.Server - MetricsAddr string -} - -func setupMetricsServer(metricAddress string) *http.Server { - return &http.Server{ - Addr: metricAddress, - Handler: promhttp.Handler(), - } -} - -func (p *Helper) StartMetricsServer(metricsAddr string) { - p.metricsServer = setupMetricsServer(metricsAddr) - - klog.Infof("started: prometheus metrics server on %s", metricsAddr) - - err := p.metricsServer.ListenAndServe() - if !errors.Is(err, http.ErrServerClosed) { - // Error starting or closing listener - klog.Errorf("error starting prometheus metrics server: %s", err) - os.Exit(1) - } -} - -var ( - ID = uuid.New().String() - InboundConnCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "inbound_connection_count", - Help: "The total number of inbound connections established", - }, - []string{"id"}, - ) - OutboundConnCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "outbound_connection_count", - Help: "The total number of outbound connections established", - }, - []string{"id"}, - ) - InboundBytesCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "inbound_bytes_count", - Help: "The total number of bytes sent and received on inbound connections", - }, - []string{"id"}, - ) - OutboundBytesCounter = prometheus.NewCounterVec( - prometheus.CounterOpts{ - Name: "outbound_bytes_count", - Help: "The total number of bytes sent and received on outbound connections", - }, - []string{"id"}, - ) - ActiveInboundConnCount int64 = 0 - ActiveInboundConnGauge = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "active_inbound_connections", - Help: "The number of currently active inbound connections", - }, - []string{"id"}, - ) - ActiveOutboundConnCount int64 = 0 - ActiveOutboundConnGauge = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "active_outbound_connections", - Help: "The number of currently active outbound connections", - }, - []string{"id"}, - ) -) - -func IncrementActiveInboundGauge() { - InboundConnCounter.WithLabelValues(ID).Inc() - atomic.AddInt64(&ActiveInboundConnCount, 1) - ActiveInboundConnGauge.WithLabelValues(ID).Inc() -} - -func IncrementActiveOutboundGauge() { - OutboundConnCounter.WithLabelValues(ID).Inc() - atomic.AddInt64(&ActiveOutboundConnCount, 1) - ActiveOutboundConnGauge.WithLabelValues(ID).Inc() -} - -func DecrementActiveInboundGauge() { - atomic.AddInt64(&ActiveInboundConnCount, -1) - ActiveInboundConnGauge.WithLabelValues(ID).Dec() -} - -func DecrementActiveOutboundGauge() { - atomic.AddInt64(&ActiveOutboundConnCount, -1) - ActiveOutboundConnGauge.WithLabelValues(ID).Dec() -} - -func UpdateBytesReceivedCounter(inboundBytesCopied uint64) { - InboundBytesCounter.WithLabelValues(ID).Add(float64(inboundBytesCopied)) -} -func UpdateBytesSentCounter(outboundBytesCopied uint64) { - OutboundBytesCounter.WithLabelValues(ID).Add(float64(outboundBytesCopied)) -} diff --git a/pkg/tcp-proxy/proxy/proxy.go b/pkg/tcp-proxy/proxy/proxy.go deleted file mode 100644 index d5f0137b5..000000000 --- a/pkg/tcp-proxy/proxy/proxy.go +++ /dev/null @@ -1,167 +0,0 @@ -/******************************************************************************* -* MIT License -* -* Copyright (c) 2020 dev@jpillora.com -* -* Permission is hereby granted, free of charge, to any person obtaining a copy -* of this software and associated documentation files (the "Software"), to deal -* in the Software without restriction, including without limitation the rights -* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -* copies of the Software, and to permit persons to whom the Software is -* furnished to do so, subject to the following conditions: -* -* The above copyright notice and this permission notice shall be included in all -* copies or substantial portions of the Software. -* -* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -* SOFTWARE. -*******************************************************************************/ - -package proxy - -import ( - "crypto/tls" - "errors" - "io" - "net" - - "k8s.io/klog/v2" - - "github.com/cloudoperators/greenhouse/pkg/tcp-proxy/metrics" -) - -// Proxy - Manages a Proxy connection, piping data between local and remote. -type Proxy struct { - sentBytes uint64 - receivedBytes uint64 - laddr, raddr *net.TCPAddr - lconn, rconn io.ReadWriteCloser - erred bool - errsig chan bool - tlsUnwrapp bool - tlsAddress string - - // Settings - OutputHex bool -} - -// New - Create a new Proxy instance. Takes over local connection passed in, -// and closes it when finished. -func New(lconn *net.TCPConn, laddr, raddr *net.TCPAddr) *Proxy { - return &Proxy{ - lconn: lconn, - laddr: laddr, - raddr: raddr, - erred: false, - errsig: make(chan bool), - } -} - -// NewTLSUnwrapped - Create a new Proxy instance with a remote TLS server for -// which we want to unwrap the TLS to be able to connect without encryption -// locally -func NewTLSUnwrapped(lconn *net.TCPConn, laddr, raddr *net.TCPAddr, addr string) *Proxy { - p := New(lconn, laddr, raddr) - p.tlsUnwrapp = true - p.tlsAddress = addr - return p -} - -// Start - open connection to remote and start proxying data. -func (p *Proxy) Start() { - defer p.lconn.Close() - - var err error - // connect to remote - if p.tlsUnwrapp { - p.rconn, err = tls.Dial("tcp", p.tlsAddress, nil) - } else { - p.rconn, err = net.DialTCP("tcp", nil, p.raddr) - } - if err != nil { - klog.Errorf("Remote connection failed: %s", err) - // Inbound connection has been closed, so decrement active inbound gauge - metrics.DecrementActiveInboundGauge() - return - } - // Outbound connection established, so increment active outbound gauge - metrics.IncrementActiveOutboundGauge() - defer p.rconn.Close() - - // display both ends - klog.Infof("Opened %s >>> %s", p.laddr.String(), p.raddr.String()) - - // bidirectional copy - go p.pipe(p.lconn, p.rconn) - go p.pipe(p.rconn, p.lconn) - - // wait for close... - <-p.errsig - klog.Infof("Closed (%d bytes sent, %d bytes received)", p.sentBytes, p.receivedBytes) - // Connection proxying complete, so update all metrics - metrics.UpdateBytesReceivedCounter(p.receivedBytes) - metrics.UpdateBytesSentCounter(p.sentBytes) - metrics.DecrementActiveInboundGauge() - metrics.DecrementActiveOutboundGauge() -} - -func (p *Proxy) err(s string, err error) { - if p.erred { - return - } - if !errors.Is(err, io.EOF) { - klog.Errorf(s, err) - } - p.errsig <- true - p.erred = true -} - -func (p *Proxy) pipe(src, dst io.ReadWriter) { - islocal := src == p.lconn - - var dataDirection string - if islocal { - dataDirection = ">>> %d bytes sent%s" - } else { - dataDirection = "<<< %d bytes received%s" - } - - var byteFormat string - if p.OutputHex { - byteFormat = "%x" - } else { - byteFormat = "%s" - } - - // directional copy (64k buffer) - buff := make([]byte, 0xffff) - for { - n, err := src.Read(buff) - if err != nil { - p.err("Read failed '%s'\n", err) - return - } - b := buff[:n] - - // show output - klog.V(5).Infof(dataDirection, n, "") - klog.V(5).Infof(byteFormat, b) - - // write out result - n, err = dst.Write(b) - if err != nil { - p.err("Write failed '%s'\n", err) - return - } - if islocal { - p.sentBytes += uint64(n) - } else { - p.receivedBytes += uint64(n) - } - } -} diff --git a/pkg/test/headscale.go b/pkg/test/headscale.go deleted file mode 100644 index 58c22d4a9..000000000 --- a/pkg/test/headscale.go +++ /dev/null @@ -1,236 +0,0 @@ -// SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Greenhouse contributors -// SPDX-License-Identifier: Apache-2.0 - -package test - -import ( - "context" - "errors" - - v1 "github.com/juanfont/headscale/gen/go/headscale/v1" - "google.golang.org/grpc" - "k8s.io/cli-runtime/pkg/genericclioptions" - "sigs.k8s.io/controller-runtime/pkg/client" - - "github.com/cloudoperators/greenhouse/pkg/clientutil" -) - -// FakeHeadscaleClient is a fake implementation of the HeadscaleClient interface. -// -//nolint:stylecheck -type FakeHeadscaleClient struct { - // IsUserDeleted is used to simulate the deletion of a user. If set to true, the GetUserFunc will return an error. - IsUserDeleted bool - - CreateApiKeyFunc func(context.Context, *v1.CreateApiKeyRequest, ...grpc.CallOption) (*v1.CreateApiKeyResponse, error) // no-lint:stylecheck - CreatePreAuthKeyFunc func(context.Context, *v1.CreatePreAuthKeyRequest, ...grpc.CallOption) (*v1.CreatePreAuthKeyResponse, error) - CreateUserFunc func(context.Context, *v1.CreateUserRequest, ...grpc.CallOption) (*v1.CreateUserResponse, error) - DebugCreateMachineFunc func(context.Context, *v1.DebugCreateMachineRequest, ...grpc.CallOption) (*v1.DebugCreateMachineResponse, error) - DeleteMachineFunc func(context.Context, *v1.DeleteMachineRequest, ...grpc.CallOption) (*v1.DeleteMachineResponse, error) - DeleteRouteFunc func(context.Context, *v1.DeleteRouteRequest, ...grpc.CallOption) (*v1.DeleteRouteResponse, error) - DeleteUserFunc func(context.Context, *v1.DeleteUserRequest, ...grpc.CallOption) (*v1.DeleteUserResponse, error) - DisableRouteFunc func(context.Context, *v1.DisableRouteRequest, ...grpc.CallOption) (*v1.DisableRouteResponse, error) - EnableRouteFunc func(context.Context, *v1.EnableRouteRequest, ...grpc.CallOption) (*v1.EnableRouteResponse, error) - ExpireApiKeyFunc func(context.Context, *v1.ExpireApiKeyRequest, ...grpc.CallOption) (*v1.ExpireApiKeyResponse, error) // no-lint:stylecheck - ExpireMachineFunc func(context.Context, *v1.ExpireMachineRequest, ...grpc.CallOption) (*v1.ExpireMachineResponse, error) - ExpirePreAuthKeyFunc func(context.Context, *v1.ExpirePreAuthKeyRequest, ...grpc.CallOption) (*v1.ExpirePreAuthKeyResponse, error) - GetMachineFunc func(context.Context, *v1.GetMachineRequest, ...grpc.CallOption) (*v1.GetMachineResponse, error) - GetMachineRoutesFunc func(context.Context, *v1.GetMachineRoutesRequest, ...grpc.CallOption) (*v1.GetMachineRoutesResponse, error) - GetRoutesFunc func(context.Context, *v1.GetRoutesRequest, ...grpc.CallOption) (*v1.GetRoutesResponse, error) - GetUserFunc func(context.Context, *v1.GetUserRequest, ...grpc.CallOption) (*v1.GetUserResponse, error) - ListApiKeysFunc func(context.Context, *v1.ListApiKeysRequest, ...grpc.CallOption) (*v1.ListApiKeysResponse, error) - ListMachinesFunc func(context.Context, *v1.ListMachinesRequest, ...grpc.CallOption) (*v1.ListMachinesResponse, error) - ListPreAuthKeysFunc func(context.Context, *v1.ListPreAuthKeysRequest, ...grpc.CallOption) (*v1.ListPreAuthKeysResponse, error) - ListUsersFunc func(context.Context, *v1.ListUsersRequest, ...grpc.CallOption) (*v1.ListUsersResponse, error) - MoveMachineFunc func(context.Context, *v1.MoveMachineRequest, ...grpc.CallOption) (*v1.MoveMachineResponse, error) - RegisterMachineFunc func(context.Context, *v1.RegisterMachineRequest, ...grpc.CallOption) (*v1.RegisterMachineResponse, error) - RenameMachineFunc func(context.Context, *v1.RenameMachineRequest, ...grpc.CallOption) (*v1.RenameMachineResponse, error) - RenameUserFunc func(context.Context, *v1.RenameUserRequest, ...grpc.CallOption) (*v1.RenameUserResponse, error) - SetTagsFunc func(context.Context, *v1.SetTagsRequest, ...grpc.CallOption) (*v1.SetTagsResponse, error) -} - -func (f FakeHeadscaleClient) CreateUser(ctx context.Context, in *v1.CreateUserRequest, opts ...grpc.CallOption) (*v1.CreateUserResponse, error) { - if f.CreateUserFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to CreateUser") - } - return f.CreateUserFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) DeleteUser(ctx context.Context, in *v1.DeleteUserRequest, opts ...grpc.CallOption) (*v1.DeleteUserResponse, error) { - if f.DeleteUserFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to DeleteUser") - } - return f.DeleteUserFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) GetUser(ctx context.Context, in *v1.GetUserRequest, opts ...grpc.CallOption) (*v1.GetUserResponse, error) { - if f.GetUserFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to GetUser") - } - return f.GetUserFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) CreatePreAuthKey(ctx context.Context, in *v1.CreatePreAuthKeyRequest, opts ...grpc.CallOption) (*v1.CreatePreAuthKeyResponse, error) { - if f.CreatePreAuthKeyFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to CreatePreAuthKey") - } - return f.CreatePreAuthKeyFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) DeleteMachine(ctx context.Context, in *v1.DeleteMachineRequest, opts ...grpc.CallOption) (*v1.DeleteMachineResponse, error) { - if f.DeleteMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to DeleteMachine") - } - return f.DeleteMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) ListMachines(ctx context.Context, in *v1.ListMachinesRequest, opts ...grpc.CallOption) (*v1.ListMachinesResponse, error) { - if f.ListMachinesFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ListMachines") - } - return f.ListMachinesFunc(ctx, in, opts...) -} - -//nolint:stylecheck -func (f FakeHeadscaleClient) CreateApiKey(ctx context.Context, in *v1.CreateApiKeyRequest, opts ...grpc.CallOption) (*v1.CreateApiKeyResponse, error) { - if f.CreateApiKeyFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to CreateApiKey") - } - return f.CreateApiKeyFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) DebugCreateMachine(ctx context.Context, in *v1.DebugCreateMachineRequest, opts ...grpc.CallOption) (*v1.DebugCreateMachineResponse, error) { - if f.DebugCreateMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to DebugCreateMachine") - } - return f.DebugCreateMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) DeleteRoute(ctx context.Context, in *v1.DeleteRouteRequest, opts ...grpc.CallOption) (*v1.DeleteRouteResponse, error) { - if f.DeleteRouteFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to DeleteRoute") - } - return f.DeleteRouteFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) DisableRoute(ctx context.Context, in *v1.DisableRouteRequest, opts ...grpc.CallOption) (*v1.DisableRouteResponse, error) { - if f.DisableRouteFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to DisableRoute") - } - return f.DisableRouteFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) EnableRoute(ctx context.Context, in *v1.EnableRouteRequest, opts ...grpc.CallOption) (*v1.EnableRouteResponse, error) { - if f.EnableRouteFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to EnableRoute") - } - return f.EnableRouteFunc(ctx, in, opts...) -} - -//nolint:stylecheck -func (f FakeHeadscaleClient) ExpireApiKey(ctx context.Context, in *v1.ExpireApiKeyRequest, opts ...grpc.CallOption) (*v1.ExpireApiKeyResponse, error) { - if f.ExpireApiKeyFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ExpireApiKey") - } - return f.ExpireApiKeyFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) ExpireMachine(ctx context.Context, in *v1.ExpireMachineRequest, opts ...grpc.CallOption) (*v1.ExpireMachineResponse, error) { - if f.ExpireMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ExpireMachine") - } - return f.ExpireMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) ExpirePreAuthKey(ctx context.Context, in *v1.ExpirePreAuthKeyRequest, opts ...grpc.CallOption) (*v1.ExpirePreAuthKeyResponse, error) { - if f.ExpirePreAuthKeyFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ExpirePreAuthKey") - } - return f.ExpirePreAuthKeyFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) GetMachine(ctx context.Context, in *v1.GetMachineRequest, opts ...grpc.CallOption) (*v1.GetMachineResponse, error) { - if f.GetMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to GetMachine") - } - return f.GetMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) GetMachineRoutes(ctx context.Context, in *v1.GetMachineRoutesRequest, opts ...grpc.CallOption) (*v1.GetMachineRoutesResponse, error) { - if f.GetMachineRoutesFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to GetMachineRoutes") - } - return f.GetMachineRoutesFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) GetRoutes(ctx context.Context, in *v1.GetRoutesRequest, opts ...grpc.CallOption) (*v1.GetRoutesResponse, error) { - if f.GetRoutesFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to GetRoutes") - } - return f.GetRoutesFunc(ctx, in, opts...) -} - -//nolint:stylecheck -func (f FakeHeadscaleClient) ListApiKeys(ctx context.Context, in *v1.ListApiKeysRequest, opts ...grpc.CallOption) (*v1.ListApiKeysResponse, error) { - if f.ListApiKeysFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ListApiKeys") - } - return f.ListApiKeysFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) ListPreAuthKeys(ctx context.Context, in *v1.ListPreAuthKeysRequest, opts ...grpc.CallOption) (*v1.ListPreAuthKeysResponse, error) { - if f.ListPreAuthKeysFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ListPreAuthKeys") - } - return f.ListPreAuthKeysFunc(ctx, in, opts...) -} -func (f FakeHeadscaleClient) ListUsers(ctx context.Context, in *v1.ListUsersRequest, opts ...grpc.CallOption) (*v1.ListUsersResponse, error) { - if f.ListUsersFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to ListUsers") - } - return f.ListUsersFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) MoveMachine(ctx context.Context, in *v1.MoveMachineRequest, opts ...grpc.CallOption) (*v1.MoveMachineResponse, error) { - if f.MoveMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to MoveMachines") - } - return f.MoveMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) RegisterMachine(ctx context.Context, in *v1.RegisterMachineRequest, opts ...grpc.CallOption) (*v1.RegisterMachineResponse, error) { - if f.RegisterMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to RegisterMachine") - } - return f.RegisterMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) RenameMachine(ctx context.Context, in *v1.RenameMachineRequest, opts ...grpc.CallOption) (*v1.RenameMachineResponse, error) { - if f.RenameMachineFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to RenameMachine") - } - return f.RenameMachineFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) RenameUser(ctx context.Context, in *v1.RenameUserRequest, opts ...grpc.CallOption) (*v1.RenameUserResponse, error) { - if f.RenameUserFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to RenameUser") - } - return f.RenameUserFunc(ctx, in, opts...) -} - -func (f FakeHeadscaleClient) SetTags(ctx context.Context, in *v1.SetTagsRequest, opts ...grpc.CallOption) (*v1.SetTagsResponse, error) { - if f.SetTagsFunc == nil { - return nil, errors.New("FakeHeadscaleClient was not configured to respond to SetTags") - } - return f.SetTagsFunc(ctx, in, opts...) -} - -// DummyTailscaleClienGetter is a dummy tailscale client getter for testing purposes. As we do not have a headscale setup for testing, we need to mock the tailscale client getter. -func DummyTailscaleClientGetter(restClientGetter genericclioptions.RESTClientGetter, proxy, headscaleAddress string) (client.Client, error) { - cfg, err := restClientGetter.ToRESTConfig() - if err != nil { - return nil, err - } - return clientutil.NewK8sClient(cfg) -} diff --git a/test/e2e/controllers.go b/test/e2e/controllers.go index 5fec478ae..fc3caa2d0 100644 --- a/test/e2e/controllers.go +++ b/test/e2e/controllers.go @@ -57,7 +57,6 @@ var knownControllers = map[string]func(controllerName string, mgr ctrl.Manager) "bootStrap": (&clustercontrollers.BootstrapReconciler{}).SetupWithManager, "clusterDirectAccess": startClusterDirectAccessReconciler, // "clusterPropagation": (&clustercontrollers.ClusterPropagationReconciler{}).SetupWithManager, - // "clusterHeadscaleAccess": startClusterHeadscaleAccessReconciler, "clusterStatus": (&clustercontrollers.ClusterStatusReconciler{}).SetupWithManager, } diff --git a/types/typescript/schema.d.ts b/types/typescript/schema.d.ts index a663b7a2d..305c7fdbe 100644 --- a/types/typescript/schema.d.ts +++ b/types/typescript/schema.d.ts @@ -9,7 +9,7 @@ */ export interface paths { - "/Plugin": { + "/Organization": { parameters: { query?: never; header?: never; @@ -27,7 +27,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description Plugin */ + /** @description Organization */ default: { headers: { [name: string]: unknown; @@ -42,7 +42,7 @@ export interface paths { patch?: never; trace?: never; }; - "/Organization": { + "/ClusterKubeconfig": { parameters: { query?: never; header?: never; @@ -60,7 +60,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description Organization */ + /** @description ClusterKubeconfig */ default: { headers: { [name: string]: unknown; @@ -75,7 +75,7 @@ export interface paths { patch?: never; trace?: never; }; - "/TeamRole": { + "/Cluster": { parameters: { query?: never; header?: never; @@ -93,7 +93,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description TeamRole */ + /** @description Cluster */ default: { headers: { [name: string]: unknown; @@ -108,7 +108,7 @@ export interface paths { patch?: never; trace?: never; }; - "/PluginPreset": { + "/TeamRole": { parameters: { query?: never; header?: never; @@ -126,7 +126,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description PluginPreset */ + /** @description TeamRole */ default: { headers: { [name: string]: unknown; @@ -141,7 +141,7 @@ export interface paths { patch?: never; trace?: never; }; - "/Cluster": { + "/TeamMembership": { parameters: { query?: never; header?: never; @@ -159,7 +159,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description Cluster */ + /** @description TeamMembership */ default: { headers: { [name: string]: unknown; @@ -174,7 +174,7 @@ export interface paths { patch?: never; trace?: never; }; - "/ClusterKubeconfig": { + "/PluginPreset": { parameters: { query?: never; header?: never; @@ -192,7 +192,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description ClusterKubeconfig */ + /** @description PluginPreset */ default: { headers: { [name: string]: unknown; @@ -207,7 +207,7 @@ export interface paths { patch?: never; trace?: never; }; - "/PluginDefinition": { + "/Team": { parameters: { query?: never; header?: never; @@ -225,7 +225,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description PluginDefinition */ + /** @description Team */ default: { headers: { [name: string]: unknown; @@ -240,7 +240,7 @@ export interface paths { patch?: never; trace?: never; }; - "/Team": { + "/TeamRoleBinding": { parameters: { query?: never; header?: never; @@ -258,7 +258,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description Team */ + /** @description TeamRoleBinding */ default: { headers: { [name: string]: unknown; @@ -273,7 +273,7 @@ export interface paths { patch?: never; trace?: never; }; - "/TeamMembership": { + "/Plugin": { parameters: { query?: never; header?: never; @@ -291,7 +291,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description TeamMembership */ + /** @description Plugin */ default: { headers: { [name: string]: unknown; @@ -306,7 +306,7 @@ export interface paths { patch?: never; trace?: never; }; - "/TeamRoleBinding": { + "/PluginDefinition": { parameters: { query?: never; header?: never; @@ -324,7 +324,7 @@ export interface paths { }; requestBody?: never; responses: { - /** @description TeamRoleBinding */ + /** @description PluginDefinition */ default: { headers: { [name: string]: unknown; @@ -343,142 +343,6 @@ export interface paths { export type webhooks = Record; export interface components { schemas: { - /** - * Plugin - * @description Plugin is the Schema for the plugins API - */ - Plugin: { - /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; - }; - }; - /** @description PluginSpec defines the desired state of Plugin */ - spec?: { - /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ - clusterName?: string; - /** @description Disabled indicates that the plugin is administratively disabled. */ - disabled: boolean; - /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ - displayName?: string; - /** @description Values are the values for a PluginDefinition instance. */ - optionValues?: { - /** @description Name of the values. */ - name: string; - /** @description Value is the actual value in plain text. */ - value?: unknown; - /** @description ValueFrom references a potentially confidential value in another source. */ - valueFrom?: { - /** @description Secret references the secret containing the value. */ - secret?: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - }; - }[]; - /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ - pluginDefinition: string; - /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ - releaseNamespace?: string; - }; - /** @description PluginStatus defines the observed state of Plugin */ - status?: { - /** @description Description provides additional details of the plugin. */ - description?: string; - /** @description ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. */ - exposedServices?: { - [key: string]: { - /** @description Name is the name of the service in the target cluster. */ - name: string; - /** @description Namespace is the namespace of the service in the target cluster. */ - namespace: string; - /** - * Format: int32 - * @description Port is the port of the service. - */ - port: number; - /** @description Protocol is the protocol of the service. */ - protocol?: string; - }; - }; - /** @description HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. */ - helmChart?: { - /** @description Name of the HelmChart chart. */ - name: string; - /** @description Repository of the HelmChart chart. */ - repository: string; - /** @description Version of the HelmChart chart. */ - version: string; - }; - /** @description HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. */ - helmReleaseStatus?: { - /** - * Format: date-time - * @description FirstDeployed is the timestamp of the first deployment of the release. - */ - firstDeployed?: string; - /** - * Format: date-time - * @description LastDeployed is the timestamp of the last deployment of the release. - */ - lastDeployed?: string; - /** @description Status is the status of a HelmChart release. */ - status: string; - }; - /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; - }; - /** @description UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. */ - uiApplication?: { - /** @description Name of the UI application. */ - name: string; - /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ - url?: string; - /** @description Version of the frontend application. */ - version: string; - }; - /** @description Version contains the latest pluginDefinition version the config was last applied with successfully. */ - version?: string; - /** - * Format: int32 - * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. - */ - weight?: number; - }; - }; /** * Organization * @description Organization is the Schema for the organizations API @@ -567,10 +431,10 @@ export interface components { status?: Record; }; /** - * TeamRole - * @description TeamRole is the Schema for the TeamRoles API + * ClusterKubeconfig + * @description ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name */ - TeamRole: { + ClusterKubeconfig: { /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -592,162 +456,46 @@ export interface components { [key: string]: string; }; }; - /** @description TeamRoleSpec defines the desired state of a TeamRole */ + /** @description ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig */ spec?: { - /** @description AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster */ - aggregationRule?: { - /** @description ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added */ - clusterRoleSelectors?: { - /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ - matchExpressions?: { - /** @description key is the label key that the selector applies to. */ - key: string; - /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ - operator: string; - /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ - values?: string[]; - }[]; - /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ - matchLabels?: { - [key: string]: string; + /** @description ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config */ + kubeconfig?: { + apiVersion?: string; + clusters?: { + cluster: { + /** Format: byte */ + "certificate-authority-data"?: string; + server?: string; + }; + name: string; + }[]; + contexts: { + context?: { + cluster: string; + namespace?: string; + user: string; + }; + name: string; + }[]; + "current-context"?: string; + kind?: string; + preferences?: Record; + users: { + name: string; + user?: { + /** @description AuthProviderConfig holds the configuration for a specified auth provider. */ + "auth-provider"?: { + config?: { + [key: string]: string; + }; + name: string; + }; + /** Format: byte */ + "client-certificate-data"?: string; + /** Format: byte */ + "client-key-data"?: string; }; }[]; - }; - /** @description Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles */ - labels?: { - [key: string]: string; - }; - /** @description Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role */ - rules?: { - /** @description APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. */ - apiGroups?: string[]; - /** @description NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. */ - nonResourceURLs?: string[]; - /** @description ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. */ - resourceNames?: string[]; - /** @description Resources is a list of resources this rule applies to. '*' represents all resources. */ - resources?: string[]; - /** @description Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. */ - verbs: string[]; - }[]; - }; - /** @description TeamRoleStatus defines the observed state of a TeamRole */ - status?: Record; - }; - /** - * PluginPreset - * @description PluginPreset is the Schema for the PluginPresets API - */ - PluginPreset: { - /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ - apiVersion?: string; - /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ - kind?: string; - metadata?: { - name?: string; - namespace?: string; - /** Format: uuid */ - uid?: string; - resourceVersion?: string; - /** Format: date-time */ - creationTimestamp?: string; - /** Format: date-time */ - deletionTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; - }; - }; - /** @description PluginPresetSpec defines the desired state of PluginPreset */ - spec?: { - /** @description ClusterOptionOverrides define plugin option values to override by the PluginPreset */ - clusterOptionOverrides?: { - clusterName: string; - overrides: { - /** @description Name of the values. */ - name: string; - /** @description Value is the actual value in plain text. */ - value?: unknown; - /** @description ValueFrom references a potentially confidential value in another source. */ - valueFrom?: { - /** @description Secret references the secret containing the value. */ - secret?: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - }; - }[]; - }[]; - /** @description ClusterSelector is a label selector to select the clusters the plugin bundle should be deployed to. */ - clusterSelector: { - /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ - matchExpressions?: { - /** @description key is the label key that the selector applies to. */ - key: string; - /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ - operator: string; - /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ - values?: string[]; - }[]; - /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ - matchLabels?: { - [key: string]: string; - }; - }; - /** @description PluginSpec is the spec of the plugin to be deployed by the PluginPreset. */ - plugin: { - /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ - clusterName?: string; - /** @description Disabled indicates that the plugin is administratively disabled. */ - disabled: boolean; - /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ - displayName?: string; - /** @description Values are the values for a PluginDefinition instance. */ - optionValues?: { - /** @description Name of the values. */ - name: string; - /** @description Value is the actual value in plain text. */ - value?: unknown; - /** @description ValueFrom references a potentially confidential value in another source. */ - valueFrom?: { - /** @description Secret references the secret containing the value. */ - secret?: { - /** @description Key in the secret to select the value from. */ - key: string; - /** @description Name of the secret in the same namespace. */ - name: string; - }; - }; - }[]; - /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ - pluginDefinition: string; - /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ - releaseNamespace?: string; - }; - }; - /** @description PluginPresetStatus defines the observed state of PluginPreset */ - status?: { - /** @description StatusConditions contain the different conditions that constitute the status of the PluginPreset. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; }; }; }; @@ -783,7 +531,7 @@ export interface components { * @description AccessMode configures how the cluster is accessed from the Greenhouse operator. * @enum {string} */ - accessMode: "direct" | "headscale"; + accessMode: "direct"; }; /** @description ClusterStatus defines the observed state of Cluster */ status?: { @@ -792,31 +540,6 @@ export interface components { * @description BearerTokenExpirationTimestamp reflects the expiration timestamp of the bearer token used to access the cluster. */ bearerTokenExpirationTimestamp?: string; - /** @description HeadScaleStatus contains the current status of the headscale client. */ - headScaleStatus?: { - /** Format: date-time */ - createdAt?: string; - /** Format: date-time */ - expiry?: string; - forcedTags?: string[]; - /** Format: int64 */ - id?: number; - ipAddresses?: string[]; - name?: string; - online?: boolean; - /** @description PreAuthKey reflects the status of the pre-authentication key used by the Headscale machine. */ - preAuthKey?: { - /** Format: date-time */ - createdAt?: string; - ephemeral?: boolean; - /** Format: date-time */ - expiration?: string; - id?: string; - reusable?: boolean; - used?: boolean; - user?: string; - }; - }; /** @description KubernetesVersion reflects the detected Kubernetes version of the cluster. */ kubernetesVersion?: string; /** @description Nodes provides a map of cluster node names to node statuses */ @@ -865,10 +588,10 @@ export interface components { }; }; /** - * ClusterKubeconfig - * @description ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name + * TeamRole + * @description TeamRole is the Schema for the TeamRoles API */ - ClusterKubeconfig: { + TeamRole: { /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -890,54 +613,53 @@ export interface components { [key: string]: string; }; }; - /** @description ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig */ + /** @description TeamRoleSpec defines the desired state of a TeamRole */ spec?: { - /** @description ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config */ - kubeconfig?: { - apiVersion?: string; - clusters?: { - cluster: { - /** Format: byte */ - "certificate-authority-data"?: string; - server?: string; - }; - name: string; - }[]; - contexts: { - context?: { - cluster: string; - namespace?: string; - user: string; - }; - name: string; - }[]; - "current-context"?: string; - kind?: string; - preferences?: Record; - users: { - name: string; - user?: { - /** @description AuthProviderConfig holds the configuration for a specified auth provider. */ - "auth-provider"?: { - config?: { - [key: string]: string; - }; - name: string; - }; - /** Format: byte */ - "client-certificate-data"?: string; - /** Format: byte */ - "client-key-data"?: string; + /** @description AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster */ + aggregationRule?: { + /** @description ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added */ + clusterRoleSelectors?: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string; + /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ + operator: string; + /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ + values?: string[]; + }[]; + /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ + matchLabels?: { + [key: string]: string; }; }[]; }; + /** @description Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles */ + labels?: { + [key: string]: string; + }; + /** @description Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role */ + rules?: { + /** @description APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. */ + apiGroups?: string[]; + /** @description NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. */ + nonResourceURLs?: string[]; + /** @description ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. */ + resourceNames?: string[]; + /** @description Resources is a list of resources this rule applies to. '*' represents all resources. */ + resources?: string[]; + /** @description Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. */ + verbs: string[]; + }[]; }; + /** @description TeamRoleStatus defines the observed state of a TeamRole */ + status?: Record; }; /** - * PluginDefinition - * @description PluginDefinition is the Schema for the PluginDefinitions API + * TeamMembership + * @description TeamMembership is the Schema for the teammemberships API */ - PluginDefinition: { + TeamMembership: { /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -959,70 +681,57 @@ export interface components { [key: string]: string; }; }; - /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ + /** @description TeamMembershipSpec defines the desired state of TeamMembership */ spec?: { - /** @description Description provides additional details of the pluginDefinition. */ - description?: string; - /** @description DisplayName provides a human-readable label for the pluginDefinition. */ - displayName?: string; - /** @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. */ - docMarkDownUrl?: string; - /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ - helmChart?: { - /** @description Name of the HelmChart chart. */ - name: string; - /** @description Repository of the HelmChart chart. */ - repository: string; - /** @description Version of the HelmChart chart. */ - version: string; - }; - /** @description Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px */ - icon?: string; - /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ - options?: { - /** @description Default provides a default value for the option */ - default?: unknown; - /** @description Description provides a human-readable text for the value as shown in the UI. */ - description?: string; - /** @description DisplayName provides a human-readable label for the configuration option */ - displayName?: string; - /** @description Name/Key of the config option. */ - name: string; - /** @description Regex specifies a match rule for validating configuration options. */ - regex?: string; - /** @description Required indicates that this config option is required */ - required: boolean; - /** - * @description Type of this configuration option. - * @enum {string} - */ - type: "string" | "secret" | "bool" | "int" | "list" | "map"; + /** @description Members list users that are part of a team. */ + members?: { + /** @description Email of the user. */ + email: string; + /** @description FirstName of the user. */ + firstName: string; + /** @description ID is the unique identifier of the user. */ + id: string; + /** @description LastName of the user. */ + lastName: string; }[]; - /** @description UIApplication specifies a reference to a UI application */ - uiApplication?: { - /** @description Name of the UI application. */ - name: string; - /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ - url?: string; - /** @description Version of the frontend application. */ - version: string; - }; - /** @description Version of this pluginDefinition */ - version: string; + }; + /** @description TeamMembershipStatus defines the observed state of TeamMembership */ + status?: { /** - * Format: int32 - * @description Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. + * Format: date-time + * @description LastSyncedTime is the information when was the last time the membership was synced */ - weight?: number; + lastSyncedTime?: string; + /** + * Format: date-time + * @description LastChangedTime is the information when was the last time the membership was actually changed + */ + lastUpdateTime?: string; + /** @description StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; }; - /** @description PluginDefinitionStatus defines the observed state of PluginDefinition */ - status?: Record; }; /** - * Team - * @description Team is the Schema for the teams API + * PluginPreset + * @description PluginPreset is the Schema for the PluginPresets API */ - Team: { + PluginPreset: { /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -1044,21 +753,102 @@ export interface components { [key: string]: string; }; }; - /** @description TeamSpec defines the desired state of Team */ + /** @description PluginPresetSpec defines the desired state of PluginPreset */ spec?: { - /** @description Description provides additional details of the team. */ - description?: string; - /** @description IdP group id matching team. */ - mappedIdPGroup?: string; + /** @description ClusterOptionOverrides define plugin option values to override by the PluginPreset */ + clusterOptionOverrides?: { + clusterName: string; + overrides: { + /** @description Name of the values. */ + name: string; + /** @description Value is the actual value in plain text. */ + value?: unknown; + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + }; + }[]; + }[]; + /** @description ClusterSelector is a label selector to select the clusters the plugin bundle should be deployed to. */ + clusterSelector: { + /** @description matchExpressions is a list of label selector requirements. The requirements are ANDed. */ + matchExpressions?: { + /** @description key is the label key that the selector applies to. */ + key: string; + /** @description operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. */ + operator: string; + /** @description values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. */ + values?: string[]; + }[]; + /** @description matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. */ + matchLabels?: { + [key: string]: string; + }; + }; + /** @description PluginSpec is the spec of the plugin to be deployed by the PluginPreset. */ + plugin: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string; + /** @description Disabled indicates that the plugin is administratively disabled. */ + disabled: boolean; + /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ + displayName?: string; + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string; + /** @description Value is the actual value in plain text. */ + value?: unknown; + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + }; + }[]; + /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ + pluginDefinition: string; + /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ + releaseNamespace?: string; + }; + }; + /** @description PluginPresetStatus defines the observed state of PluginPreset */ + status?: { + /** @description StatusConditions contain the different conditions that constitute the status of the PluginPreset. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; }; - /** @description TeamStatus defines the observed state of Team */ - status?: Record; }; /** - * TeamMembership - * @description TeamMembership is the Schema for the teammemberships API + * Team + * @description Team is the Schema for the teams API */ - TeamMembership: { + Team: { /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ apiVersion?: string; /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ @@ -1080,51 +870,15 @@ export interface components { [key: string]: string; }; }; - /** @description TeamMembershipSpec defines the desired state of TeamMembership */ + /** @description TeamSpec defines the desired state of Team */ spec?: { - /** @description Members list users that are part of a team. */ - members?: { - /** @description Email of the user. */ - email: string; - /** @description FirstName of the user. */ - firstName: string; - /** @description ID is the unique identifier of the user. */ - id: string; - /** @description LastName of the user. */ - lastName: string; - }[]; - }; - /** @description TeamMembershipStatus defines the observed state of TeamMembership */ - status?: { - /** - * Format: date-time - * @description LastSyncedTime is the information when was the last time the membership was synced - */ - lastSyncedTime?: string; - /** - * Format: date-time - * @description LastChangedTime is the information when was the last time the membership was actually changed - */ - lastUpdateTime?: string; - /** @description StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. */ - statusConditions?: { - conditions?: { - /** - * Format: date-time - * @description LastTransitionTime is the last time the condition transitioned from one status to another. - */ - lastTransitionTime: string; - /** @description Message is an optional human readable message indicating details about the last transition. */ - message?: string; - /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ - reason?: string; - /** @description Status of the condition. */ - status: string; - /** @description Type of the condition. */ - type: string; - }[]; - }; + /** @description Description provides additional details of the team. */ + description?: string; + /** @description IdP group id matching team. */ + mappedIdPGroup?: string; }; + /** @description TeamStatus defines the observed state of Team */ + status?: Record; }; /** * TeamRoleBinding @@ -1222,6 +976,227 @@ export interface components { }; }; }; + /** + * Plugin + * @description Plugin is the Schema for the plugins API + */ + Plugin: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description PluginSpec defines the desired state of Plugin */ + spec?: { + /** @description ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. */ + clusterName?: string; + /** @description Disabled indicates that the plugin is administratively disabled. */ + disabled: boolean; + /** @description DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. */ + displayName?: string; + /** @description Values are the values for a PluginDefinition instance. */ + optionValues?: { + /** @description Name of the values. */ + name: string; + /** @description Value is the actual value in plain text. */ + value?: unknown; + /** @description ValueFrom references a potentially confidential value in another source. */ + valueFrom?: { + /** @description Secret references the secret containing the value. */ + secret?: { + /** @description Key in the secret to select the value from. */ + key: string; + /** @description Name of the secret in the same namespace. */ + name: string; + }; + }; + }[]; + /** @description PluginDefinition is the name of the PluginDefinition this instance is for. */ + pluginDefinition: string; + /** @description ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. */ + releaseNamespace?: string; + }; + /** @description PluginStatus defines the observed state of Plugin */ + status?: { + /** @description Description provides additional details of the plugin. */ + description?: string; + /** @description ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. */ + exposedServices?: { + [key: string]: { + /** @description Name is the name of the service in the target cluster. */ + name: string; + /** @description Namespace is the namespace of the service in the target cluster. */ + namespace: string; + /** + * Format: int32 + * @description Port is the port of the service. + */ + port: number; + /** @description Protocol is the protocol of the service. */ + protocol?: string; + }; + }; + /** @description HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string; + /** @description Repository of the HelmChart chart. */ + repository: string; + /** @description Version of the HelmChart chart. */ + version: string; + }; + /** @description HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. */ + helmReleaseStatus?: { + /** + * Format: date-time + * @description FirstDeployed is the timestamp of the first deployment of the release. + */ + firstDeployed?: string; + /** + * Format: date-time + * @description LastDeployed is the timestamp of the last deployment of the release. + */ + lastDeployed?: string; + /** @description Status is the status of a HelmChart release. */ + status: string; + }; + /** @description StatusConditions contain the different conditions that constitute the status of the Plugin. */ + statusConditions?: { + conditions?: { + /** + * Format: date-time + * @description LastTransitionTime is the last time the condition transitioned from one status to another. + */ + lastTransitionTime: string; + /** @description Message is an optional human readable message indicating details about the last transition. */ + message?: string; + /** @description Reason is a one-word, CamelCase reason for the condition's last transition. */ + reason?: string; + /** @description Status of the condition. */ + status: string; + /** @description Type of the condition. */ + type: string; + }[]; + }; + /** @description UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string; + /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ + url?: string; + /** @description Version of the frontend application. */ + version: string; + }; + /** @description Version contains the latest pluginDefinition version the config was last applied with successfully. */ + version?: string; + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI. + */ + weight?: number; + }; + }; + /** + * PluginDefinition + * @description PluginDefinition is the Schema for the PluginDefinitions API + */ + PluginDefinition: { + /** @description APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources */ + apiVersion?: string; + /** @description Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds */ + kind?: string; + metadata?: { + name?: string; + namespace?: string; + /** Format: uuid */ + uid?: string; + resourceVersion?: string; + /** Format: date-time */ + creationTimestamp?: string; + /** Format: date-time */ + deletionTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + /** @description PluginDefinitionSpec defines the desired state of PluginDefinitionSpec */ + spec?: { + /** @description Description provides additional details of the pluginDefinition. */ + description?: string; + /** @description DisplayName provides a human-readable label for the pluginDefinition. */ + displayName?: string; + /** @description DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. */ + docMarkDownUrl?: string; + /** @description HelmChart specifies where the Helm Chart for this pluginDefinition can be found. */ + helmChart?: { + /** @description Name of the HelmChart chart. */ + name: string; + /** @description Repository of the HelmChart chart. */ + repository: string; + /** @description Version of the HelmChart chart. */ + version: string; + }; + /** @description Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px */ + icon?: string; + /** @description RequiredValues is a list of values required to create an instance of this PluginDefinition. */ + options?: { + /** @description Default provides a default value for the option */ + default?: unknown; + /** @description Description provides a human-readable text for the value as shown in the UI. */ + description?: string; + /** @description DisplayName provides a human-readable label for the configuration option */ + displayName?: string; + /** @description Name/Key of the config option. */ + name: string; + /** @description Regex specifies a match rule for validating configuration options. */ + regex?: string; + /** @description Required indicates that this config option is required */ + required: boolean; + /** + * @description Type of this configuration option. + * @enum {string} + */ + type: "string" | "secret" | "bool" | "int" | "list" | "map"; + }[]; + /** @description UIApplication specifies a reference to a UI application */ + uiApplication?: { + /** @description Name of the UI application. */ + name: string; + /** @description URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. */ + url?: string; + /** @description Version of the frontend application. */ + version: string; + }; + /** @description Version of this pluginDefinition */ + version: string; + /** + * Format: int32 + * @description Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. + */ + weight?: number; + }; + /** @description PluginDefinitionStatus defines the observed state of PluginDefinition */ + status?: Record; + }; }; responses: never; parameters: never; From 777142306823d0f0f0ea08c1125d8ac72ece6b34 Mon Sep 17 00:00:00 2001 From: CRD API Docs Bot Date: Thu, 10 Oct 2024 11:46:13 +0000 Subject: [PATCH 2/2] Automatic generation of CRD API Docs --- docs/reference/api/openapi.yaml | 1100 +++++++++++++++---------------- 1 file changed, 550 insertions(+), 550 deletions(-) diff --git a/docs/reference/api/openapi.yaml b/docs/reference/api/openapi.yaml index 43e51dabb..519340b77 100755 --- a/docs/reference/api/openapi.yaml +++ b/docs/reference/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.0.0 info: title: Greenhouse - version: b6c8a6d + version: 723b1e3 description: PlusOne operations platform paths: /Organization: @@ -9,21 +9,21 @@ paths: responses: default: description: Organization - /ClusterKubeconfig: + /Plugin: post: responses: default: - description: ClusterKubeconfig + description: Plugin /Cluster: post: responses: default: description: Cluster - /TeamRole: + /ClusterKubeconfig: post: responses: default: - description: TeamRole + description: ClusterKubeconfig /TeamMembership: post: responses: @@ -34,26 +34,26 @@ paths: responses: default: description: PluginPreset - /Team: + /PluginDefinition: post: responses: default: - description: Team - /TeamRoleBinding: + description: PluginDefinition + /TeamRole: post: responses: default: - description: TeamRoleBinding - /Plugin: + description: TeamRole + /Team: post: responses: default: - description: Plugin - /PluginDefinition: + description: Team + /TeamRoleBinding: post: responses: default: - description: PluginDefinition + description: TeamRoleBinding components: schemas: Organization: @@ -177,12 +177,12 @@ components: description: OrganizationStatus defines the observed state of an Organization type: object type: object - ClusterKubeconfig: + Plugin: xml: name: greenhouse.sap namespace: v1alpha1 - title: ClusterKubeconfig - description: ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name + title: Plugin + description: Plugin is the Schema for the plugins API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -193,92 +193,179 @@ components: metadata: type: object spec: - description: ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig + description: PluginSpec defines the desired state of Plugin properties: - kubeconfig: - description: 'ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config' - properties: - apiVersion: - type: string - clusters: - items: - properties: - cluster: - properties: - certificate-authority-data: - format: byte - type: string - server: - type: string - type: object - name: - type: string - required: - - cluster - - name - type: object - type: array - contexts: - items: + clusterName: + description: ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. + type: string + disabled: + description: Disabled indicates that the plugin is administratively disabled. + type: boolean + displayName: + description: DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. + type: string + optionValues: + description: Values are the values for a PluginDefinition instance. + items: + description: PluginOptionValue is the value for a PluginOption. + properties: + name: + description: Name of the values. + type: string + value: + description: Value is the actual value in plain text. + x-kubernetes-preserve-unknown-fields: true + valueFrom: + description: ValueFrom references a potentially confidential value in another source. properties: - context: + secret: + description: Secret references the secret containing the value. properties: - cluster: - type: string - namespace: + key: + description: Key in the secret to select the value from. type: string - user: + name: + description: Name of the secret in the same namespace. type: string required: - - cluster - - user + - key + - name type: object - name: - type: string - required: - - name type: object - type: array - current-context: + required: + - name + type: object + type: array + pluginDefinition: + description: PluginDefinition is the name of the PluginDefinition this instance is for. + type: string + releaseNamespace: + description: ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. + type: string + required: + - disabled + - pluginDefinition + type: object + status: + description: PluginStatus defines the observed state of Plugin + properties: + description: + description: Description provides additional details of the plugin. + type: string + exposedServices: + additionalProperties: + description: Service references a Kubernetes service of a Plugin. + properties: + name: + description: Name is the name of the service in the target cluster. + type: string + namespace: + description: Namespace is the namespace of the service in the target cluster. + type: string + port: + description: Port is the port of the service. + format: int32 + type: integer + protocol: + description: Protocol is the protocol of the service. + type: string + required: + - name + - namespace + - port + type: object + description: ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. + type: object + helmChart: + description: HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. + properties: + name: + description: Name of the HelmChart chart. type: string - kind: + repository: + description: Repository of the HelmChart chart. type: string - preferences: - type: object - users: + version: + description: Version of the HelmChart chart. + type: string + required: + - name + - repository + - version + type: object + helmReleaseStatus: + description: HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. + properties: + firstDeployed: + description: FirstDeployed is the timestamp of the first deployment of the release. + format: date-time + type: string + lastDeployed: + description: LastDeployed is the timestamp of the last deployment of the release. + format: date-time + type: string + status: + description: Status is the status of a HelmChart release. + type: string + required: + - status + type: object + statusConditions: + description: StatusConditions contain the different conditions that constitute the status of the Plugin. + properties: + conditions: items: + description: Condition contains additional information on the state of a resource. properties: - name: + lastTransitionTime: + description: LastTransitionTime is the last time the condition transitioned from one status to another. + format: date-time + type: string + message: + description: Message is an optional human readable message indicating details about the last transition. + type: string + reason: + description: Reason is a one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status of the condition. + type: string + type: + description: Type of the condition. type: string - user: - properties: - auth-provider: - description: AuthProviderConfig holds the configuration for a specified auth provider. - properties: - config: - additionalProperties: - type: string - type: object - name: - type: string - required: - - name - type: object - client-certificate-data: - format: byte - type: string - client-key-data: - format: byte - type: string - type: object required: - - name + - lastTransitionTime + - status + - type type: object type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + type: object + uiApplication: + description: UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. + properties: + name: + description: Name of the UI application. + type: string + url: + description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. + type: string + version: + description: Version of the frontend application. + type: string required: - - contexts - - users + - name + - version type: object + version: + description: Version contains the latest pluginDefinition version the config was last applied with successfully. + type: string + weight: + description: Weight configures the order in which Plugins are shown in the Greenhouse UI. + format: int32 + type: integer type: object type: object Cluster: @@ -394,12 +481,12 @@ components: type: object type: object type: object - TeamRole: + ClusterKubeconfig: xml: name: greenhouse.sap namespace: v1alpha1 - title: TeamRole - description: TeamRole is the Schema for the TeamRoles API + title: ClusterKubeconfig + description: ClusterKubeconfig is the Schema for the clusterkubeconfigs API\nObjectMeta.OwnerReferences is used to link the ClusterKubeconfig to the Cluster\nObjectMeta.Generation is used to detect changes in the ClusterKubeconfig and sync local kubeconfig files\nObjectMeta.Name is designed to be the same with the Cluster name properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -410,96 +497,92 @@ components: metadata: type: object spec: - description: TeamRoleSpec defines the desired state of a TeamRole + description: ClusterKubeconfigSpec stores the kubeconfig data for the cluster\nThe idea is to use kubeconfig data locally with minimum effort (with local tools or plain kubectl):\nkubectl get cluster-kubeconfig $NAME -o yaml | yq -y .spec.kubeconfig properties: - aggregationRule: - description: AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster + kubeconfig: + description: 'ClusterKubeconfigData stores the kubeconfig data ready to use kubectl or other local tooling\nIt is a simplified version of clientcmdapi.Config: https://pkg.go.dev/k8s.io/client-go/tools/clientcmd/api#Config' properties: - clusterRoleSelectors: - description: ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added + apiVersion: + type: string + clusters: items: - description: A label selector is a label query over a set of resources. The result of matchLabels and\nmatchExpressions are ANDed. An empty label selector matches all objects. A null\nlabel selector matches no objects. properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. - items: + cluster: + properties: + certificate-authority-data: + format: byte + type: string + server: + type: string + type: object + name: + type: string + required: + - cluster + - name + type: object + type: array + contexts: + items: + properties: + context: + properties: + cluster: + type: string + namespace: + type: string + user: + type: string + required: + - cluster + - user + type: object + name: + type: string + required: + - name + type: object + type: array + current-context: + type: string + kind: + type: string + preferences: + type: object + users: + items: + properties: + name: + type: string + user: + properties: + auth-provider: + description: AuthProviderConfig holds the configuration for a specified auth provider. + properties: + config: + additionalProperties: + type: string + type: object + name: type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. + required: + - name + type: object + client-certificate-data: + format: byte + type: string + client-key-data: + format: byte + type: string type: object + required: + - name type: object - x-kubernetes-map-type: atomic type: array - x-kubernetes-list-type: atomic - type: object - labels: - additionalProperties: - type: string - description: Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles + required: + - contexts + - users type: object - rules: - description: Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role - items: - description: PolicyRule holds information that describes a policy rule, but does not contain information\nabout who the rule applies to or which namespace the rule applies to. - properties: - apiGroups: - description: APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. - items: - type: string - type: array - x-kubernetes-list-type: atomic - nonResourceURLs: - description: NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. - items: - type: string - type: array - x-kubernetes-list-type: atomic - resourceNames: - description: ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. - items: - type: string - type: array - x-kubernetes-list-type: atomic - resources: - description: Resources is a list of resources this rule applies to. '*' represents all resources. - items: - type: string - type: array - x-kubernetes-list-type: atomic - verbs: - description: Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - verbs - type: object - type: array - type: object - status: - description: TeamRoleStatus defines the observed state of a TeamRole type: object type: object TeamMembership: @@ -781,12 +864,12 @@ components: type: object type: object type: object - Team: + PluginDefinition: xml: name: greenhouse.sap namespace: v1alpha1 - title: Team - description: Team is the Schema for the teams API + title: PluginDefinition + description: PluginDefinition is the Schema for the PluginDefinitions API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -797,168 +880,219 @@ components: metadata: type: object spec: - description: TeamSpec defines the desired state of Team + description: PluginDefinitionSpec defines the desired state of PluginDefinitionSpec properties: description: - description: Description provides additional details of the team. + description: Description provides additional details of the pluginDefinition. type: string - mappedIdPGroup: - description: IdP group id matching team. + displayName: + description: DisplayName provides a human-readable label for the pluginDefinition. type: string - type: object - status: - description: TeamStatus defines the observed state of Team - type: object - type: object - TeamRoleBinding: - xml: - name: greenhouse.sap - namespace: v1alpha1 - title: TeamRoleBinding - description: TeamRoleBinding is the Schema for the rolebindings API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TeamRoleBindingSpec defines the desired state of a TeamRoleBinding - properties: - clusterName: - description: ClusterName is the name of the cluster the rbacv1 resources are created on. + docMarkDownUrl: + description: DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. type: string - clusterSelector: - description: ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to. + helmChart: + description: HelmChart specifies where the Helm Chart for this pluginDefinition can be found. properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. - items: - type: string - type: array - x-kubernetes-list-type: atomic - required: - - key - - operator - type: object - type: array - x-kubernetes-list-type: atomic - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. - type: object + name: + description: Name of the HelmChart chart. + type: string + repository: + description: Repository of the HelmChart chart. + type: string + version: + description: Version of the HelmChart chart. + type: string + required: + - name + - repository + - version type: object - x-kubernetes-map-type: atomic - namespaces: - description: Namespaces is the immutable list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.\nIf empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace. + icon: + description: 'Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px' + type: string + options: + description: RequiredValues is a list of values required to create an instance of this PluginDefinition. items: - type: string - type: array - teamRef: - description: TeamRef references a Greenhouse Team by name - type: string - teamRoleRef: - description: TeamRoleRef references a Greenhouse TeamRole by name - type: string - type: object - status: - description: TeamRoleBindingStatus defines the observed state of the TeamRoleBinding - properties: - clusters: - description: PropagationStatus is the list of clusters the TeamRoleBinding is applied to - items: - description: PropagationStatus defines the observed state of the TeamRoleBinding's associated rbacv1 resources on a Cluster properties: - clusterName: - description: ClusterName is the name of the cluster the rbacv1 resources are created on. + default: + description: Default provides a default value for the option + x-kubernetes-preserve-unknown-fields: true + description: + description: Description provides a human-readable text for the value as shown in the UI. + type: string + displayName: + description: DisplayName provides a human-readable label for the configuration option + type: string + name: + description: Name/Key of the config option. + type: string + regex: + description: Regex specifies a match rule for validating configuration options. + type: string + required: + description: Required indicates that this config option is required + type: boolean + type: + description: Type of this configuration option. + enum: + - string + - secret + - bool + - int + - list + - map type: string - condition: - description: Condition is the overall Status of the rbacv1 resources created on the cluster - properties: - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - message: - description: Message is an optional human readable message indicating details about the last transition. - type: string - reason: - description: Reason is a one-word, CamelCase reason for the condition's last transition. - type: string - status: - description: Status of the condition. - type: string - type: - description: Type of the condition. - type: string - required: - - lastTransitionTime - - status - - type - type: object required: - - clusterName + - name + - required + - type type: object type: array - x-kubernetes-list-map-keys: - - clusterName - x-kubernetes-list-type: map - statusConditions: - description: StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. + uiApplication: + description: UIApplication specifies a reference to a UI application properties: - conditions: + name: + description: Name of the UI application. + type: string + url: + description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. + type: string + version: + description: Version of the frontend application. + type: string + required: + - name + - version + type: object + version: + description: Version of this pluginDefinition + type: string + weight: + description: Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. + format: int32 + type: integer + required: + - version + type: object + status: + description: PluginDefinitionStatus defines the observed state of PluginDefinition + type: object + type: object + TeamRole: + xml: + name: greenhouse.sap + namespace: v1alpha1 + title: TeamRole + description: TeamRole is the Schema for the TeamRoles API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TeamRoleSpec defines the desired state of a TeamRole + properties: + aggregationRule: + description: AggregationRule describes how to locate ClusterRoles to aggregate into the ClusterRole on the remote cluster + properties: + clusterRoleSelectors: + description: ClusterRoleSelectors holds a list of selectors which will be used to find ClusterRoles and create the rules.\nIf any of the selectors match, then the ClusterRole's permissions will be added items: - description: Condition contains additional information on the state of a resource. + description: A label selector is a label query over a set of resources. The result of matchLabels and\nmatchExpressions are ANDed. An empty label selector matches all objects. A null\nlabel selector matches no objects. properties: - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - message: - description: Message is an optional human readable message indicating details about the last transition. - type: string - reason: - description: Reason is a one-word, CamelCase reason for the condition's last transition. - type: string - status: - description: Status of the condition. - type: string - type: - description: Type of the condition. - type: string - required: - - lastTransitionTime - - status - - type + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. + properties: + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator + type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. + type: object type: object + x-kubernetes-map-type: atomic type: array - x-kubernetes-list-map-keys: - - type - x-kubernetes-list-type: map + x-kubernetes-list-type: atomic + type: object + labels: + additionalProperties: + type: string + description: Labels are applied to the ClusterRole created on the remote cluster.\nThis allows using TeamRoles as part of AggregationRules by other TeamRoles type: object + rules: + description: Rules is a list of rbacv1.PolicyRules used on a managed RBAC (Cluster)Role + items: + description: PolicyRule holds information that describes a policy rule, but does not contain information\nabout who the rule applies to or which namespace the rule applies to. + properties: + apiGroups: + description: APIGroups is the name of the APIGroup that contains the resources. If multiple API groups are specified, any action requested against one of\nthe enumerated resources in any API group will be allowed. "" represents the core API group and "*" represents all API groups. + items: + type: string + type: array + x-kubernetes-list-type: atomic + nonResourceURLs: + description: NonResourceURLs is a set of partial urls that a user should have access to. *s are allowed, but only as the full, final step in the path\nSince non-resource URLs are not namespaced, this field is only applicable for ClusterRoles referenced from a ClusterRoleBinding.\nRules can either apply to API resources (such as "pods" or "secrets") or non-resource URL paths (such as "/api"), but not both. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resourceNames: + description: ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed. + items: + type: string + type: array + x-kubernetes-list-type: atomic + resources: + description: Resources is a list of resources this rule applies to. '*' represents all resources. + items: + type: string + type: array + x-kubernetes-list-type: atomic + verbs: + description: Verbs is a list of Verbs that apply to ALL the ResourceKinds contained in this rule. '*' represents all verbs. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - verbs + type: object + type: array + type: object + status: + description: TeamRoleStatus defines the observed state of a TeamRole type: object type: object - Plugin: + Team: xml: name: greenhouse.sap namespace: v1alpha1 - title: Plugin - description: Plugin is the Schema for the plugins API + title: Team + description: Team is the Schema for the teams API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' @@ -969,125 +1103,129 @@ components: metadata: type: object spec: - description: PluginSpec defines the desired state of Plugin + description: TeamSpec defines the desired state of Team properties: - clusterName: - description: ClusterName is the name of the cluster the plugin is deployed to. If not set, the plugin is deployed to the greenhouse cluster. + description: + description: Description provides additional details of the team. type: string - disabled: - description: Disabled indicates that the plugin is administratively disabled. - type: boolean - displayName: - description: DisplayName is an optional name for the Plugin to be displayed in the Greenhouse UI.\nThis is especially helpful to distinguish multiple instances of a PluginDefinition in the same context.\nDefaults to a normalized version of metadata.name. + mappedIdPGroup: + description: IdP group id matching team. type: string - optionValues: - description: Values are the values for a PluginDefinition instance. - items: - description: PluginOptionValue is the value for a PluginOption. - properties: - name: - description: Name of the values. - type: string - value: - description: Value is the actual value in plain text. - x-kubernetes-preserve-unknown-fields: true - valueFrom: - description: ValueFrom references a potentially confidential value in another source. + type: object + status: + description: TeamStatus defines the observed state of Team + type: object + type: object + TeamRoleBinding: + xml: + name: greenhouse.sap + namespace: v1alpha1 + title: TeamRoleBinding + description: TeamRoleBinding is the Schema for the rolebindings API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: TeamRoleBindingSpec defines the desired state of a TeamRoleBinding + properties: + clusterName: + description: ClusterName is the name of the cluster the rbacv1 resources are created on. + type: string + clusterSelector: + description: ClusterSelector is a label selector to select the Clusters the TeamRoleBinding should be deployed to. + properties: + matchExpressions: + description: matchExpressions is a list of label selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector that contains values, a key, and an operator that\nrelates the key and values. properties: - secret: - description: Secret references the secret containing the value. - properties: - key: - description: Key in the secret to select the value from. - type: string - name: - description: Name of the secret in the same namespace. - type: string - required: - - key - - name - type: object + key: + description: key is the label key that the selector applies to. + type: string + operator: + description: operator represents a key's relationship to a set of values.\nValid operators are In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. If the operator is In or NotIn,\nthe values array must be non-empty. If the operator is Exists or DoesNotExist,\nthe values array must be empty. This array is replaced during a strategic\nmerge patch. + items: + type: string + type: array + x-kubernetes-list-type: atomic + required: + - key + - operator type: object - required: - - name - type: object + type: array + x-kubernetes-list-type: atomic + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels\nmap is equivalent to an element of matchExpressions, whose key field is "key", the\noperator is "In", and the values array contains only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-map-type: atomic + namespaces: + description: Namespaces is the immutable list of namespaces in the Greenhouse Clusters to apply the RoleBinding to.\nIf empty, a ClusterRoleBinding will be created on the remote cluster, otherwise a RoleBinding per namespace. + items: + type: string type: array - pluginDefinition: - description: PluginDefinition is the name of the PluginDefinition this instance is for. + teamRef: + description: TeamRef references a Greenhouse Team by name type: string - releaseNamespace: - description: ReleaseNamespace is the namespace in the remote cluster to which the backend is deployed.\nDefaults to the Greenhouse managed namespace if not set. + teamRoleRef: + description: TeamRoleRef references a Greenhouse TeamRole by name type: string - required: - - disabled - - pluginDefinition type: object status: - description: PluginStatus defines the observed state of Plugin + description: TeamRoleBindingStatus defines the observed state of the TeamRoleBinding properties: - description: - description: Description provides additional details of the plugin. - type: string - exposedServices: - additionalProperties: - description: Service references a Kubernetes service of a Plugin. + clusters: + description: PropagationStatus is the list of clusters the TeamRoleBinding is applied to + items: + description: PropagationStatus defines the observed state of the TeamRoleBinding's associated rbacv1 resources on a Cluster properties: - name: - description: Name is the name of the service in the target cluster. - type: string - namespace: - description: Namespace is the namespace of the service in the target cluster. - type: string - port: - description: Port is the port of the service. - format: int32 - type: integer - protocol: - description: Protocol is the protocol of the service. + clusterName: + description: ClusterName is the name of the cluster the rbacv1 resources are created on. type: string + condition: + description: Condition is the overall Status of the rbacv1 resources created on the cluster + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition transitioned from one status to another. + format: date-time + type: string + message: + description: Message is an optional human readable message indicating details about the last transition. + type: string + reason: + description: Reason is a one-word, CamelCase reason for the condition's last transition. + type: string + status: + description: Status of the condition. + type: string + type: + description: Type of the condition. + type: string + required: + - lastTransitionTime + - status + - type + type: object required: - - name - - namespace - - port + - clusterName type: object - description: ExposedServices provides an overview of the Plugins services that are centrally exposed.\nIt maps the exposed URL to the service found in the manifest. - type: object - helmChart: - description: HelmChart contains a reference the helm chart used for the deployed pluginDefinition version. - properties: - name: - description: Name of the HelmChart chart. - type: string - repository: - description: Repository of the HelmChart chart. - type: string - version: - description: Version of the HelmChart chart. - type: string - required: - - name - - repository - - version - type: object - helmReleaseStatus: - description: HelmReleaseStatus reflects the status of the latest HelmChart release.\nThis is only configured if the pluginDefinition is backed by HelmChart. - properties: - firstDeployed: - description: FirstDeployed is the timestamp of the first deployment of the release. - format: date-time - type: string - lastDeployed: - description: LastDeployed is the timestamp of the last deployment of the release. - format: date-time - type: string - status: - description: Status is the status of a HelmChart release. - type: string - required: - - status - type: object + type: array + x-kubernetes-list-map-keys: + - clusterName + x-kubernetes-list-type: map statusConditions: - description: StatusConditions contain the different conditions that constitute the status of the Plugin. + description: StatusConditions contain the different conditions that constitute the status of the TeamRoleBinding. properties: conditions: items: @@ -1119,143 +1257,5 @@ components: - type x-kubernetes-list-type: map type: object - uiApplication: - description: UIApplication contains a reference to the frontend that is used for the deployed pluginDefinition version. - properties: - name: - description: Name of the UI application. - type: string - url: - description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. - type: string - version: - description: Version of the frontend application. - type: string - required: - - name - - version - type: object - version: - description: Version contains the latest pluginDefinition version the config was last applied with successfully. - type: string - weight: - description: Weight configures the order in which Plugins are shown in the Greenhouse UI. - format: int32 - type: integer - type: object - type: object - PluginDefinition: - xml: - name: greenhouse.sap - namespace: v1alpha1 - title: PluginDefinition - description: PluginDefinition is the Schema for the PluginDefinitions API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object.\nServers should convert recognized schemas to the latest internal value, and\nmay reject unrecognized values.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents.\nServers may infer this from the endpoint the client submits requests to.\nCannot be updated.\nIn CamelCase.\nMore info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: PluginDefinitionSpec defines the desired state of PluginDefinitionSpec - properties: - description: - description: Description provides additional details of the pluginDefinition. - type: string - displayName: - description: DisplayName provides a human-readable label for the pluginDefinition. - type: string - docMarkDownUrl: - description: DocMarkDownUrl specifies the URL to the markdown documentation file for this plugin.\nSource needs to allow all CORS origins. - type: string - helmChart: - description: HelmChart specifies where the Helm Chart for this pluginDefinition can be found. - properties: - name: - description: Name of the HelmChart chart. - type: string - repository: - description: Repository of the HelmChart chart. - type: string - version: - description: Version of the HelmChart chart. - type: string - required: - - name - - repository - - version - type: object - icon: - description: 'Icon specifies the icon to be used for this plugin in the Greenhouse UI.\nIcons can be either:\n- A string representing a juno icon in camel case from this list: https://github.com/sapcc/juno/blob/main/libs/juno-ui-components/src/components/Icon/Icon.component.js#L6-L52\n- A publicly accessible image reference to a .png file. Will be displayed 100x100px' - type: string - options: - description: RequiredValues is a list of values required to create an instance of this PluginDefinition. - items: - properties: - default: - description: Default provides a default value for the option - x-kubernetes-preserve-unknown-fields: true - description: - description: Description provides a human-readable text for the value as shown in the UI. - type: string - displayName: - description: DisplayName provides a human-readable label for the configuration option - type: string - name: - description: Name/Key of the config option. - type: string - regex: - description: Regex specifies a match rule for validating configuration options. - type: string - required: - description: Required indicates that this config option is required - type: boolean - type: - description: Type of this configuration option. - enum: - - string - - secret - - bool - - int - - list - - map - type: string - required: - - name - - required - - type - type: object - type: array - uiApplication: - description: UIApplication specifies a reference to a UI application - properties: - name: - description: Name of the UI application. - type: string - url: - description: URL specifies the url to a built javascript asset.\nBy default, assets are loaded from the Juno asset server using the provided name and version. - type: string - version: - description: Version of the frontend application. - type: string - required: - - name - - version - type: object - version: - description: Version of this pluginDefinition - type: string - weight: - description: Weight configures the order in which Plugins are shown in the Greenhouse UI.\nDefaults to alphabetical sorting if not provided or on conflict. - format: int32 - type: integer - required: - - version - type: object - status: - description: PluginDefinitionStatus defines the observed state of PluginDefinition type: object type: object