From 6e9f37bc1b8d256ec71e3ada9586a8fe8b39a23c Mon Sep 17 00:00:00 2001 From: Onur Yilmaz Date: Mon, 7 Oct 2019 13:15:21 +0000 Subject: [PATCH] Kubernetes dashboard for kubernikus clusters --- charts/images.yaml | 45 + charts/kube-master/templates/_helpers.tpl | 35 + charts/kube-master/templates/api.yaml | 7 + charts/kube-master/templates/dashboard.yaml | 154 ++ .../kube-master/templates/dex-configmap.yaml | 111 + charts/kube-master/templates/dex.yaml | 139 ++ charts/kube-master/test-values.yaml | 4 + charts/kube-master/values.yaml | 38 + charts/kubernikus-dex/Chart.yaml | 5 + charts/kubernikus-dex/templates/_helpers.tpl | 3 + .../kubernikus-dex/templates/dex-ingress.yaml | 20 + .../templates/keystone-secret.yaml | 13 + .../kubernikus-dex/templates/ldap-secret.yaml | 9 + charts/kubernikus-dex/test-values.yaml | 21 + charts/kubernikus-dex/values.yaml | 21 + ci/pipeline.yaml | 324 +++ ci/pipeline.yaml.erb | 17 + ci/task_helm_kubernikus-dex.yaml | 49 + etc/policy-ccadmin.json | 1 + etc/policy.json | 1 + ...t_cluster_credentials_o_id_c_parameters.go | 133 ++ ...et_cluster_credentials_o_id_c_responses.go | 112 + .../client/operations/operations_client.go | 29 + .../handlers/get_cluster_credentials_oidc.go | 81 + pkg/api/handlers/update_cluster.go | 33 + pkg/api/models/kluster_spec.go | 6 + pkg/api/models/kluster_status.go | 3 + pkg/api/rest/configure.go | 1 + pkg/api/rest/embedded_spec.go | 1973 +++++++++++++++++ .../get_cluster_credentials_o_id_c.go | 73 + ...t_cluster_credentials_o_id_c_parameters.go | 84 + ...et_cluster_credentials_o_id_c_responses.go | 116 + ...t_cluster_credentials_o_id_c_urlbuilder.go | 96 + pkg/api/rest/operations/kubernikus_api.go | 14 + pkg/api/spec/embedded_spec.go | 67 + pkg/apis/kubernikus/factory.go | 4 + pkg/apis/kubernikus/v1/secret.go | 3 + pkg/client/kubernetes/client.go | 42 + pkg/controller/ground.go | 30 +- pkg/controller/ground/bootstrap.go | 60 + pkg/migration/17_dex.go | 54 + pkg/migration/register.go | 1 + pkg/util/helm/helm.go | 38 + pkg/version/images.go | 3 + swagger.yml | 26 +- terraform/kubernikus.tf | 9 + vendor/golang.org/x/crypto/bcrypt/base64.go | 35 + vendor/golang.org/x/crypto/bcrypt/bcrypt.go | 295 +++ vendor/golang.org/x/crypto/blowfish/block.go | 159 ++ vendor/golang.org/x/crypto/blowfish/cipher.go | 91 + vendor/golang.org/x/crypto/blowfish/const.go | 199 ++ 51 files changed, 4884 insertions(+), 3 deletions(-) create mode 100644 charts/kube-master/templates/dashboard.yaml create mode 100644 charts/kube-master/templates/dex-configmap.yaml create mode 100644 charts/kube-master/templates/dex.yaml create mode 100644 charts/kubernikus-dex/Chart.yaml create mode 100644 charts/kubernikus-dex/templates/_helpers.tpl create mode 100644 charts/kubernikus-dex/templates/dex-ingress.yaml create mode 100644 charts/kubernikus-dex/templates/keystone-secret.yaml create mode 100644 charts/kubernikus-dex/templates/ldap-secret.yaml create mode 100644 charts/kubernikus-dex/test-values.yaml create mode 100644 charts/kubernikus-dex/values.yaml create mode 100644 ci/task_helm_kubernikus-dex.yaml create mode 100644 pkg/api/client/operations/get_cluster_credentials_o_id_c_parameters.go create mode 100644 pkg/api/client/operations/get_cluster_credentials_o_id_c_responses.go create mode 100644 pkg/api/handlers/get_cluster_credentials_oidc.go create mode 100644 pkg/api/rest/embedded_spec.go create mode 100644 pkg/api/rest/operations/get_cluster_credentials_o_id_c.go create mode 100644 pkg/api/rest/operations/get_cluster_credentials_o_id_c_parameters.go create mode 100644 pkg/api/rest/operations/get_cluster_credentials_o_id_c_responses.go create mode 100644 pkg/api/rest/operations/get_cluster_credentials_o_id_c_urlbuilder.go create mode 100644 pkg/migration/17_dex.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/base64.go create mode 100644 vendor/golang.org/x/crypto/bcrypt/bcrypt.go create mode 100644 vendor/golang.org/x/crypto/blowfish/block.go create mode 100644 vendor/golang.org/x/crypto/blowfish/cipher.go create mode 100644 vendor/golang.org/x/crypto/blowfish/const.go diff --git a/charts/images.yaml b/charts/images.yaml index 96bfd9624b..8879baca2a 100644 --- a/charts/images.yaml +++ b/charts/images.yaml @@ -7,6 +7,15 @@ imagesForVersion: cloudControllerManager: repository: 'sapcc/openstack-cloud-controller-manager' tag: 'v1.15.0-sap.2' + dex: + repository: 'hub.global.cloud.sap/monsoon/dex' + tag: '7abab130c718576e93f7a7cc233dfd90cb8783bb' + dashboardProxy: + repository: 'quay.io/keycloak/keycloak-gatekeeper' + tag: '6.0.1' + dashboard: + repository: 'kubernetesui/dashboard' + tag: 'v2.0.0-beta4' '1.14.5': supported: true hyperkube: @@ -15,6 +24,15 @@ imagesForVersion: cloudControllerManager: repository: 'sapcc/openstack-cloud-controller-manager' tag: 'v1.14.0-sap.0' + dex: + repository: 'hub.global.cloud.sap/monsoon/dex' + tag: '7abab130c718576e93f7a7cc233dfd90cb8783bb' + dashboardProxy: + repository: 'quay.io/keycloak/keycloak-gatekeeper' + tag: '6.0.1' + dashboard: + repository: 'kubernetesui/dashboard' + tag: 'v2.0.0-beta1' '1.13.9': supported: true hyperkube: @@ -23,17 +41,44 @@ imagesForVersion: cloudControllerManager: repository: 'sapcc/openstack-cloud-controller-manager' tag: 'v1.13.1' + dex: + repository: 'hub.global.cloud.sap/monsoon/dex' + tag: '7abab130c718576e93f7a7cc233dfd90cb8783bb' + dashboardProxy: + repository: 'quay.io/keycloak/keycloak-gatekeeper' + tag: '6.0.1' + dashboard: + repository: 'kubernetesui/dashboard' + tag: 'v2.0.0-beta1' '1.12.10': supported: true hyperkube: repository: 'sapcc/hyperkube' tag: 'v1.12.10' + dex: + repository: 'hub.global.cloud.sap/monsoon/dex' + tag: '7abab130c718576e93f7a7cc233dfd90cb8783bb' + dashboardProxy: + repository: 'quay.io/keycloak/keycloak-gatekeeper' + tag: '6.0.1' + dashboard: + repository: 'kubernetesui/dashboard' + tag: 'v2.0.0-beta1' '1.11.9': default: true supported: true hyperkube: repository: 'sapcc/hyperkube' tag: 'v1.11.9' + dex: + repository: 'hub.global.cloud.sap/monsoon/dex' + tag: '7abab130c718576e93f7a7cc233dfd90cb8783bb' + dashboardProxy: + repository: 'quay.io/keycloak/keycloak-gatekeeper' + tag: '6.0.1' + dashboard: + repository: 'k8s.gcr.io/kubernetes-dashboard-amd64' + tag: 'v1.10.1' '1.10.11': hyperkube: repository: 'sapcc/hyperkube' diff --git a/charts/kube-master/templates/_helpers.tpl b/charts/kube-master/templates/_helpers.tpl index 537e90b8f3..94b10738c9 100644 --- a/charts/kube-master/templates/_helpers.tpl +++ b/charts/kube-master/templates/_helpers.tpl @@ -35,4 +35,39 @@ We truncate at 63 chars because some Kubernetes name fields are limited to this {{- $cloudControllerManager := required (printf "No cloudControllerManager image found for version %s" $imagesForVersion) (index $imagesForVersion "cloudControllerManager") }} {{- required (printf "repository for cloudControllerManager missing for version %s" $version) $cloudControllerManager.repository }}: {{- required (printf "tag for cloudControllerManager missing for version %s" $version) $cloudControllerManager.tag }} +{{- end -}} + +{{- define "dex.image" }} +{{- $images := required "imagesForVersion undefined" .Values.imagesForVersion}} +{{- $version := required "version.kubernetes undefined" .Values.version.kubernetes }} +{{- $imagesForVersion := required (printf "unsupported kubernetes version %s" $version) (index $images $version) }} +{{- $dex := required (printf "No dex image found for version %s" $imagesForVersion) (index $imagesForVersion "dex") }} +{{- required (printf "repository for dex missing for version %s" $version) $dex.repository }}: + {{- required (printf "tag for dex missing for version %s" $version) $dex.tag }} +{{- end -}} + +{{- define "dashboard.image" }} +{{- $images := required "imagesForVersion undefined" .Values.imagesForVersion}} +{{- $version := required "version.kubernetes undefined" .Values.version.kubernetes }} +{{- $imagesForVersion := required (printf "unsupported kubernetes version %s" $version) (index $images $version) }} +{{- $dashboard := required (printf "No dashboard image found for version %s" $imagesForVersion) (index $imagesForVersion "dashboard") }} +{{- required (printf "repository for dashboard missing for version %s" $version) $dashboard.repository }}: + {{- required (printf "tag for dashboard missing for version %s" $version) $dashboard.tag }} +{{- end -}} + +{{- define "dashboardProxy.image" }} +{{- $images := required "imagesForVersion undefined" .Values.imagesForVersion}} +{{- $version := required "version.kubernetes undefined" .Values.version.kubernetes }} +{{- $imagesForVersion := required (printf "unsupported kubernetes version %s" $version) (index $images $version) }} +{{- $dashboardProxy := required (printf "No dashboardProxy image found for version %s" $imagesForVersion) (index $imagesForVersion "dashboardProxy") }} +{{- required (printf "repository for dashboardProxy missing for version %s" $version) $dashboardProxy.repository }}: + {{- required (printf "tag for dashboardProxy missing for version %s" $version) $dashboardProxy.tag }} +{{- end -}} + +{{- define "dashboard.url" -}} +{{- printf "dashboard-%s.%s.%s.%s" (include "master.fullname" .) .Values.dashboard.dns.zone .Values.openstack.region .Values.dashboard.dns.domain -}} +{{- end -}} + +{{- define "dex.url" -}} +{{- printf "auth-%s.%s.%s.%s" (include "master.fullname" .) .Values.dex.dns.zone .Values.openstack.region .Values.dex.dns.domain -}} {{- end -}} \ No newline at end of file diff --git a/charts/kube-master/templates/api.yaml b/charts/kube-master/templates/api.yaml index b17faaeaaf..ea25c8a858 100644 --- a/charts/kube-master/templates/api.yaml +++ b/charts/kube-master/templates/api.yaml @@ -176,6 +176,13 @@ spec: - --tls-cert-file=/etc/kubernetes/certs/tls-apiserver.pem - --tls-private-key-file=/etc/kubernetes/certs/tls-apiserver-key.pem # --tls-sni-cert-key=/etc/kubernetes/certs/tls-sni.pem,/etc/kubernetes/certs/tls-sni.key + {{ if .Values.dex.enabled }} + - --oidc-issuer-url=https://{{ include "dex.url" . }} + - --oidc-client-id=kubernetes + - --oidc-groups-claim=groups + - --oidc-username-prefix=- + - --oidc-username-claim=sub + {{ end }} volumeMounts: - mountPath: /etc/kubernetes/certs name: certs diff --git a/charts/kube-master/templates/dashboard.yaml b/charts/kube-master/templates/dashboard.yaml new file mode 100644 index 0000000000..b176810ff3 --- /dev/null +++ b/charts/kube-master/templates/dashboard.yaml @@ -0,0 +1,154 @@ +{{/* vim: set filetype=gotexttmpl: */ -}} +{{ if and .Values.dex.enabled .Values.dashboard.enabled }} +kind: Deployment +apiVersion: extensions/v1beta1 +metadata: + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + name: {{ include "master.fullname" . }}-dashboard +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "master.fullname" . }}-dashboard + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ include "master.fullname" . }}-dashboard + release: {{ .Release.Name }} + spec: + containers: + - image: {{ include "dashboardProxy.image" . | quote }} + name: proxy + args: + - --discovery-url=https://{{ include "dex.url" . }} # ingress of dex + - --listen=0.0.0.0:3000 # proxy address + - --enable-refresh-tokens=true + - --enable-authorization-header=true + - "--resources=uri=/*" + - --scopes=groups + - --client-id=kubernetes + - --upstream-url=http://localhost:9090 # kubernetes-dashboard in sidecar + - --redirection-url=https://{{ include "dashboard.url" . }} # ingress of dashboard + - --encryption-key={{ randAlphaNum 32 }} + env: + - name: PROXY_CLIENT_SECRET + valueFrom: + secretKeyRef: + name: {{ include "master.fullname" . }}-secret + key: dex-client-secret + ports: + - containerPort: 3000 + livenessProbe: + httpGet: + path: /oauth/health + port: 3000 + initialDelaySeconds: 60 + periodSeconds: 15 + readinessProbe: + httpGet: + path: /oauth/health + port: 3000 + initialDelaySeconds: 60 + periodSeconds: 15 + - name: dashboard + image: {{ include "dashboard.image" . | quote }} + ports: + - containerPort: 9090 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: 9090 + initialDelaySeconds: 15 + periodSeconds: 15 + readinessProbe: + httpGet: + path: / + port: 9090 + initialDelaySeconds: 15 + periodSeconds: 15 + args: + {{- if (semverCompare ">= 1.12.10" .Values.version.kubernetes) }} + - --namespace=kube-system # creates dashboard resources in the namespace, introduced in v2.0.0-beta1 + {{- end }} + - --kubeconfig=/etc/kubernetes/config/kubeconfig + {{- if (semverCompare ">= 1.15.2" .Values.version.kubernetes) }} + - --metrics-provider=none #introduced in v2.0.0-beta3 + {{- else }} + - --metric-client-check-period=2592000 # 30 days in seconds, since heapster is not installed + {{- end }} + - --enable-insecure-login # for login via header http port + volumeMounts: + # Create on-disk volume to store exec logs + - mountPath: /tmp + name: tmp-volume + - mountPath: /etc/kubernetes/certs + name: certs + readOnly: true + - mountPath: /etc/kubernetes/config + name: config + readOnly: true + volumes: + - name: tmp-volume + emptyDir: {} + - name: certs + secret: + defaultMode: 420 + items: + - key: tls-ca.pem + path: tls-ca.pem + - key: apiserver-clients-cluster-admin.pem + path: kube-client.pem + - key: apiserver-clients-cluster-admin-key.pem + path: kube-client.key + secretName: {{ include "master.fullname" . }}-secret + - configMap: + defaultMode: 420 + name: {{ include "master.fullname" . }} + name: config +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "master.fullname" . }}-dashboard + labels: + app: {{ include "master.fullname" . }}-dashboard + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} +spec: + type: ClusterIP + ports: + - protocol: TCP + port: 3000 + targetPort: 3000 + name: proxy + selector: + app: {{ include "master.fullname" . }}-dashboard +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: "nginx" + labels: + app: {{ include "master.fullname" . }}-dashboard + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + name: {{ include "master.fullname" . }}-dashboard +spec: + rules: + - host: {{ include "dashboard.url" . }} + http: + paths: + - backend: + serviceName: {{ include "master.fullname" . }}-dashboard + servicePort: 3000 + path: / + tls: + - hosts: + - {{ include "dashboard.url" . }} + secretName: {{ required "dashboard.ingressSecret undefined" .Values.dashboard.ingressSecret }} +{{ end }} \ No newline at end of file diff --git a/charts/kube-master/templates/dex-configmap.yaml b/charts/kube-master/templates/dex-configmap.yaml new file mode 100644 index 0000000000..619ba26444 --- /dev/null +++ b/charts/kube-master/templates/dex-configmap.yaml @@ -0,0 +1,111 @@ +{{/* vim: set filetype=gotexttmpl: */ -}} +{{ if .Values.dex.enabled }} +kind: ConfigMap +apiVersion: v1 +metadata: + labels: + app: {{ include "master.fullname" . }}-dex + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + name: {{ include "master.fullname" . }}-dex +data: + config.yaml: | + issuer: https://{{ include "dex.url" . }} + + storage: + type: etcd + config: + endpoints: + - http://{{ include "etcd.fullname" . }}:2379 + namespace: dex/ + + web: + http: 0.0.0.0:80 + + frontend: + theme: ccloud + issuer: "Converged Cloud Kubernetes" + + expiry: + signingKeys: "6h" + idTokens: "1h" + + logger: + level: debug + + connectors: + {{ if .Values.dex.connectors.keystone.enabled }} + - type: keystone + id: keystone + name: Converged Cloud + config: + host: {{ required "openstack.authURL undefined" .Values.openstack.authURL }} + domain: monsoon3 + adminUsername: $KEYSTONE_ADMIN_USERNAME + adminPassword: $KEYSTONE_ADMIN_PASSWORD + adminUserDomain: $KEYSTONE_ADMIN_USER_DOMAIN + adminProject: $KEYSTONE_ADMIN_PROJECT + adminDomain: $KEYSTONE_ADMIN_DOMAIN + authScope: + projectID: {{ required "openstack.projectID undefined" .Values.openstack.projectID }} # kluster project + includeRolesInGroups: true + {{ end }} + + {{ if .Values.dex.connectors.ldap.enabled }} + - type: ldap + id: ldap + name: Active Directory + config: + host: {{ required ".dex.connectors.ldap.config.host" .Values.dex.connectors.ldap.config.host }} + bindDN: {{ required "dex.connectors.ldap.config.bindDN" .Values.dex.connectors.ldap.config.bindDN }} + bindPW: $LDAP_BIND_PW + insecureSkipVerify: true + + userSearch: + baseDN: {{ required "dex.connectors.ldap.userSearch.baseDN" .Values.dex.connectors.ldap.userSearch.baseDN }} + filter: {{ required "dex.connectors.ldap.userSearch.filter" .Values.dex.connectors.ldap.userSearch.filter }} + username: cn + idAttr: distinguishedName + emailAttr: mail + nameAttr: displayName + + # Group search queries for groups given a user entry. + groupSearch: + baseDN: {{ required "dex.connectors.ldap.groupSearch.baseDN" .Values.dex.connectors.ldap.groupSearch.baseDN }} + filter: {{ required "dex.connectors.ldap.groupSearch.filter" .Values.dex.connectors.ldap.groupSearch.filter }} + + userAttr: distinguishedName + groupAttr: member + nameAttr: cn + {{ end }} + + oauth2: + skipApprovalScreen: true + responseTypes: ["code", "token", "id_token"] + {{ if .Values.dex.connectors.keystone.enabled }} + passwordConnector: keystone + {{ else if .Values.dex.connectors.ldap.enabled }} + passwordConnector: ldap + {{ else if .Values.dex.staticPasword.enabled }} + passwordConnector: local + {{ end }} + + staticClients: + - id: kubernetes + redirectURIs: + - https://{{ include "dashboard.url" . }}/oauth/callback # for dashboard access + - http://localhost:33768/auth/callback + name: kubernetes + secret: {{ required "dex.staticClientSecret" .Values.dex.staticClientSecret }} + + {{ if .Values.dex.staticPasword.enabled }} + staticPasswords: + - email: {{ required "dex.staticPasword.email" .Values.dex.staticPasword.email }} + hash: {{ required "dex.staticPasword.hashedPassword" .Values.dex.staticPasword.hashedPassword }} + username: kubernikus + userID: "00000000-0000-0000-0000-000000000001" + enablePasswordDB: true + {{ else }} + enablePasswordDB: false + {{ end }} +{{ end }} \ No newline at end of file diff --git a/charts/kube-master/templates/dex.yaml b/charts/kube-master/templates/dex.yaml new file mode 100644 index 0000000000..fb6f5382d1 --- /dev/null +++ b/charts/kube-master/templates/dex.yaml @@ -0,0 +1,139 @@ +{{/* vim: set filetype=gotexttmpl: */ -}} +{{ if .Values.dex.enabled }} +apiVersion: extensions/v1beta1 +kind: Deployment +metadata: + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + name: {{ include "master.fullname" . }}-dex +spec: + replicas: 1 + selector: + matchLabels: + app: {{ include "master.fullname" . }}-dex + release: {{ .Release.Name }} + template: + metadata: + labels: + app: {{ include "master.fullname" . }}-dex + release: {{ .Release.Name }} + annotations: + checksum/config: {{ include (print $.Template.BasePath "/dex-configmap.yaml") . | sha256sum }} + spec: + initContainers: + - name: etcd-wait + image: "{{ required "etcd.image.repository undefined" .Values.etcd.image.repository }}:{{ required "etcd.image.tag undefined" .Values.etcd.image.tag }}" + command: + - sh + - -c + args: + - until etcdctl --total-timeout=4s --endpoints http://{{ include "etcd.fullname" . }}:2379 cluster-health; do sleep 5; done; + containers: + - image: {{ include "dex.image" . | quote }} + name: dex + command: ["/usr/local/bin/dex", "serve", "/etc/dex/cfg/config.yaml"] + ports: + - name: http + containerPort: 80 + env: + {{ if .Values.dex.connectors.keystone.enabled }} + - name: KEYSTONE_ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.keystone.secret undefined" .Values.dex.connectors.keystone.secret }} + key: adminUsername + - name: KEYSTONE_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.keystone.secret undefined" .Values.dex.connectors.keystone.secret }} + key: adminPassword + - name: KEYSTONE_ADMIN_USER_DOMAIN + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.keystone.secret undefined" .Values.dex.connectors.keystone.secret }} + key: adminUserDomain + - name: KEYSTONE_ADMIN_PROJECT + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.keystone.secret undefined" .Values.dex.connectors.keystone.secret }} + key: adminProject + - name: KEYSTONE_ADMIN_DOMAIN + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.keystone.secret undefined" .Values.dex.connectors.keystone.secret }} + key: adminDomain + {{ end }} + {{ if .Values.dex.connectors.ldap.enabled }} + - name: LDAP_BIND_PW + valueFrom: + secretKeyRef: + name: {{ required "dex.connectors.ldap.secret undefined" .Values.dex.connectors.ldap.secret }} + key: bindPW + {{ end }} + livenessProbe: + httpGet: + path: /keys + port: 80 + initialDelaySeconds: 3 + timeoutSeconds: 2 + readinessProbe: + httpGet: + path: /keys + port: 80 + initialDelaySeconds: 3 + timeoutSeconds: 2 + volumeMounts: + - name: config + mountPath: /etc/dex/cfg + volumes: + - name: config + defaultMode: 420 + configMap: + name: {{ include "master.fullname" . }}-dex + items: + - key: config.yaml + path: config.yaml +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "master.fullname" . }}-dex + labels: + app: {{ include "master.fullname" . }}-dex + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} +spec: + type: ClusterIP + ports: + - protocol: TCP + port: 80 + targetPort: 80 + name: http + selector: + app: {{ include "master.fullname" . }}-dex +--- +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + kubernetes.io/ingress.class: "nginx" + labels: + app: {{ include "master.fullname" . }}-dex + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + name: {{ include "master.fullname" . }}-dex +spec: + rules: + - host: {{ include "dex.url" . }} + http: + paths: + - backend: + serviceName: {{ include "master.fullname" . }}-dex + servicePort: 80 + path: / + tls: + - hosts: + - {{ include "dex.url" . }} + secretName: {{ required "dex.ingressSecret undefined" .Values.dex.ingressSecret }} +{{ end }} \ No newline at end of file diff --git a/charts/kube-master/test-values.yaml b/charts/kube-master/test-values.yaml index b690c0f26b..3e03e7f3c6 100644 --- a/charts/kube-master/test-values.yaml +++ b/charts/kube-master/test-values.yaml @@ -32,3 +32,7 @@ imagesForVersion: hyperkube: repository: abc tag: xyz +dashboard: + enabled: false +dex: + enabled: false diff --git a/charts/kube-master/values.yaml b/charts/kube-master/values.yaml index 4b6b860671..f485fdf1a3 100644 --- a/charts/kube-master/values.yaml +++ b/charts/kube-master/values.yaml @@ -80,3 +80,41 @@ scheduler: memory: 512Mi revisionHistoryLimit: 3 + +dex: + enabled: false + connectors: + keystone: + enabled: true + secret: kubernikus-dex-keystone + ldap: + enabled: false + #config: + # host: + # bindDN: + #userSearch: + # baseDN: + # filter: + #groupSearch: + # baseDN: + # filter: + secret: kubernikus-dex-ldap + staticPasword: + enabled: false + email: kubernikus@cloud.sap + # hashedPassword: + #staticClientSecret: + dns: + zone: ingress.kubernikus + domain: cloud.sap + ingressSecret: kubernikus-dex + +dashboard: + enabled: false + + ingressSecret: kubernikus-dex + + dns: + zone: ingress.kubernikus + domain: cloud.sap + \ No newline at end of file diff --git a/charts/kubernikus-dex/Chart.yaml b/charts/kubernikus-dex/Chart.yaml new file mode 100644 index 0000000000..3d5f391143 --- /dev/null +++ b/charts/kubernikus-dex/Chart.yaml @@ -0,0 +1,5 @@ +apiVersion: v1 +appVersion: "1.0" +description: A Helm chart for Kubernetes +name: kubernikus-dex +version: 0.1.0 diff --git a/charts/kubernikus-dex/templates/_helpers.tpl b/charts/kubernikus-dex/templates/_helpers.tpl new file mode 100644 index 0000000000..da11cfb9af --- /dev/null +++ b/charts/kubernikus-dex/templates/_helpers.tpl @@ -0,0 +1,3 @@ +{{- define "dex.url" -}} +{{- printf "*.%s.%s.%s" .Values.dex.dns.zone .Values.global.region .Values.global.tld -}} +{{- end -}} diff --git a/charts/kubernikus-dex/templates/dex-ingress.yaml b/charts/kubernikus-dex/templates/dex-ingress.yaml new file mode 100644 index 0000000000..b0c1638b56 --- /dev/null +++ b/charts/kubernikus-dex/templates/dex-ingress.yaml @@ -0,0 +1,20 @@ +apiVersion: extensions/v1beta1 +kind: Ingress +metadata: + annotations: + vice-president: "true" + kubernetes.io/ingress.class: "nginx" + name: kubernikus-dex +spec: + rules: + - host: {{ include "dex.url" . | quote }} + tls: + - hosts: + - {{ include "dex.url" . | quote }} + secretName: kubernikus-dex +--- +apiVersion: v1 +kind: Secret +metadata: + name: kubernikus-dex +type: Opaque \ No newline at end of file diff --git a/charts/kubernikus-dex/templates/keystone-secret.yaml b/charts/kubernikus-dex/templates/keystone-secret.yaml new file mode 100644 index 0000000000..a45c44d1fe --- /dev/null +++ b/charts/kubernikus-dex/templates/keystone-secret.yaml @@ -0,0 +1,13 @@ + {{ if .Values.dex.connectors.keystone.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: kubernikus-dex-keystone +type: Opaque +data: + adminUsername: {{ required "api.adminUsername undefined" .Values.api.adminUsername | b64enc | quote}} + adminPassword: {{ required "api.adminPassword undefined" .Values.api.adminPassword | b64enc | quote}} + adminUserDomain: {{ required "api.adminUserDomain undefined" .Values.api.adminUserDomain | b64enc | quote}} + adminProject: {{ required "openstack.api.adminProject undefined" .Values.api.adminProject | b64enc | quote }} + adminDomain: {{ required "openstack.api.adminDomain undefined" .Values.api.adminDomain | b64enc | quote }} +{{ end }} \ No newline at end of file diff --git a/charts/kubernikus-dex/templates/ldap-secret.yaml b/charts/kubernikus-dex/templates/ldap-secret.yaml new file mode 100644 index 0000000000..aa2da14934 --- /dev/null +++ b/charts/kubernikus-dex/templates/ldap-secret.yaml @@ -0,0 +1,9 @@ + {{ if .Values.dex.connectors.ldap.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: kubernikus-dex-ldap +type: Opaque +data: + bindPW: {{ required ".Values.dex.connectors.ldap.bindPW undefined" .Values.dex.connectors.ldap.bindPW | b64enc | quote}} +{{ end }} \ No newline at end of file diff --git a/charts/kubernikus-dex/test-values.yaml b/charts/kubernikus-dex/test-values.yaml new file mode 100644 index 0000000000..7dbfb35881 --- /dev/null +++ b/charts/kubernikus-dex/test-values.yaml @@ -0,0 +1,21 @@ +# secrets/$REGION/values/global.yaml +global: + region: xyz + tld: xyz.com + +dex: + connectors: + keystone: + enabled: true + ldap: + enabled: false + bindPW: xyz + dns: + zone: xyz + +api: + adminPassword: xyz + adminUsername: xyz + adminUserDomain: xyz + adminProject: xyz + adminDomain: xyz \ No newline at end of file diff --git a/charts/kubernikus-dex/values.yaml b/charts/kubernikus-dex/values.yaml new file mode 100644 index 0000000000..a84319131c --- /dev/null +++ b/charts/kubernikus-dex/values.yaml @@ -0,0 +1,21 @@ +# secrets/$REGION/values/global.yaml +#global: +# region: eu-nl-1 +# tld: cloud.sap + +dex: + connectors: + keystone: + enabled: true + ldap: + enabled: false + # bindPW: + dns: + zone: ingress.kubernikus + +api: +# adminPassword: # from secrets/$REGION/values/keystone.yaml +# adminUsername: admin # from secrets/$REGION/values/keystone.yaml + adminUserDomain: Default + adminProject: admin + adminDomain: Default \ No newline at end of file diff --git a/ci/pipeline.yaml b/ci/pipeline.yaml index 60aa7de9c8..5471d51e19 100644 --- a/ci/pipeline.yaml +++ b/ci/pipeline.yaml @@ -799,6 +799,58 @@ task_helm-admin_kubernikus: &task_helm-admin_kubernikus GITHUB_TOKEN: +task_helm_kubernikus-dex: &task_helm_kubernikus-dex + platform: 'linux' + + image_resource: + type: registry-image + source: + repository: sapcc/kubernikus-kubectl + tag: 'v1.10.9' + + inputs: + - name: kubernikus.builds + - name: secrets.git + + run: + path: /bin/sh + args: + - -c + - | + set -exo pipefail + + # recent helm versions fails with `invalid cross-device link` when trying to rename charts.git + # rename(2) for a directory is allowed only when both the source and the destination path are on the top layer + # https://github.com/kubernetes/helm/issues/2998 + cp -a kubernikus.builds kubernikus-copy.builds + + # Dependencies are currently vendored. + # helm repo add sapcc https://charts.global.cloud.sap + helm dep up kubernikus-copy.builds/charts/kubernikus-dex/ + + helm diff upgrade kubernikus-dex kubernikus-copy.builds/charts/kubernikus-dex/ \ + --values secrets.git/$REGION/values/keystone.yaml \ + --values secrets.git/$REGION/values/global.yaml \ + --suppress-secrets --allow-unreleased --context 5 + + helm upgrade kubernikus-dex kubernikus-copy.builds/charts/kubernikus-dex/ \ + --values secrets.git/$REGION/values/keystone.yaml \ + --values secrets.git/$REGION/values/global.yaml \ + --namespace=kubernikus \ + --install + + params: + REGION: + OS_AUTH_URL: + OS_USERNAME: + OS_PASSWORD: + OS_USER_DOMAIN_NAME: + OS_PROJECT_NAME: + OS_PROJECT_DOMAIN_NAME: + KUBERNIKUS_NAME: + KUBERNIKUS_URL: + + task_helm_kubernikus-monitoring: &task_helm_kubernikus-monitoring platform: 'linux' @@ -1485,6 +1537,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-ae-1 + + - name: kubernikus-dex_ap-ae-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-ae-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-ae-1 + <<: *auth_ap-ae-1 - name: deploy_ap-ae-1 serial: true @@ -1631,6 +1699,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-au-1 + + - name: kubernikus-dex_ap-au-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-au-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-au-1 + <<: *auth_ap-au-1 - name: deploy_ap-au-1 serial: true @@ -1785,6 +1869,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-cn-1 + + - name: kubernikus-dex_ap-cn-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-cn-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-cn-1 + <<: *auth_ap-cn-1 - name: deploy_ap-cn-1 serial: true @@ -1939,6 +2039,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-jp-1 + + - name: kubernikus-dex_ap-jp-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-jp-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-jp-1 + <<: *auth_ap-jp-1 - name: deploy_ap-jp-1 serial: true @@ -2093,6 +2209,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-jp-2 + + - name: kubernikus-dex_ap-jp-2 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-jp-2 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-jp-2 + <<: *auth_ap-jp-2 - name: deploy_ap-jp-2 serial: true @@ -2247,6 +2379,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_ap-sa-1 + + - name: kubernikus-dex_ap-sa-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_ap-sa-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: ap-sa-1 + <<: *auth_ap-sa-1 - name: deploy_ap-sa-1 serial: true @@ -2458,6 +2606,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_eu-de-1 + + - name: kubernikus-dex_eu-de-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_eu-de-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: eu-de-1 + <<: *auth_eu-de-1 - name: deploy_eu-de-1 serial: true @@ -2604,6 +2768,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_eu-de-2 + + - name: kubernikus-dex_eu-de-2 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_eu-de-2 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: eu-de-2 + <<: *auth_eu-de-2 - name: deploy_eu-de-2 serial: true @@ -2750,6 +2930,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_eu-nl-1 + + - name: kubernikus-dex_eu-nl-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_eu-nl-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: eu-nl-1 + <<: *auth_eu-nl-1 - name: deploy_eu-nl-1 serial: true @@ -2904,6 +3100,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_eu-ru-1 + + - name: kubernikus-dex_eu-ru-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_eu-ru-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: eu-ru-1 + <<: *auth_eu-ru-1 - name: deploy_eu-ru-1 serial: true @@ -3131,6 +3343,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_la-br-1 + + - name: kubernikus-dex_la-br-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_la-br-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: la-br-1 + <<: *auth_la-br-1 - name: deploy_la-br-1 serial: true @@ -3285,6 +3513,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_na-ca-1 + + - name: kubernikus-dex_na-ca-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_na-ca-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: na-ca-1 + <<: *auth_na-ca-1 - name: deploy_na-ca-1 serial: true @@ -3431,6 +3675,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_na-us-1 + + - name: kubernikus-dex_na-us-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_na-us-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: na-us-1 + <<: *auth_na-us-1 - name: deploy_na-us-1 serial: true @@ -3585,6 +3845,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_na-us-2 + + - name: kubernikus-dex_na-us-2 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_na-us-2 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: na-us-2 + <<: *auth_na-us-2 - name: deploy_na-us-2 serial: true @@ -3739,6 +4015,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_na-us-3 + + - name: kubernikus-dex_na-us-3 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_na-us-3 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: na-us-3 + <<: *auth_na-us-3 - name: deploy_na-us-3 serial: true @@ -3942,6 +4234,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_qa-de-1 + + - name: kubernikus-dex_qa-de-1 + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_qa-de-1 + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: qa-de-1 + <<: *auth_qa-de-1 - name: deploy_qa-de-1 serial: true @@ -4219,96 +4527,112 @@ groups: - tiller_ap-ae-1 - deploy_ap-ae-1 - soak_ap-ae-1 + - kubernikus-dex_ap-ae-1 - seed_ap-au-1 - terraform_ap-au-1 - tiller_ap-au-1 - deploy_ap-au-1 - soak_ap-au-1 + - kubernikus-dex_ap-au-1 - seed_ap-cn-1 - terraform_ap-cn-1 - tiller_ap-cn-1 - deploy_ap-cn-1 - soak_ap-cn-1 + - kubernikus-dex_ap-cn-1 - seed_ap-jp-1 - terraform_ap-jp-1 - tiller_ap-jp-1 - deploy_ap-jp-1 - soak_ap-jp-1 + - kubernikus-dex_ap-jp-1 - seed_ap-jp-2 - terraform_ap-jp-2 - tiller_ap-jp-2 - deploy_ap-jp-2 - soak_ap-jp-2 + - kubernikus-dex_ap-jp-2 - seed_ap-sa-1 - terraform_ap-sa-1 - tiller_ap-sa-1 - deploy_ap-sa-1 - soak_ap-sa-1 + - kubernikus-dex_ap-sa-1 - seed_eu-de-1 - terraform_eu-de-1 - tiller_eu-de-1 - deploy_eu-de-1 - soak_eu-de-1 + - kubernikus-dex_eu-de-1 - seed_eu-de-2 - terraform_eu-de-2 - tiller_eu-de-2 - deploy_eu-de-2 - soak_eu-de-2 + - kubernikus-dex_eu-de-2 - seed_eu-nl-1 - terraform_eu-nl-1 - tiller_eu-nl-1 - deploy_eu-nl-1 - soak_eu-nl-1 + - kubernikus-dex_eu-nl-1 - seed_eu-ru-1 - terraform_eu-ru-1 - tiller_eu-ru-1 - deploy_eu-ru-1 - soak_eu-ru-1 + - kubernikus-dex_eu-ru-1 - seed_la-br-1 - terraform_la-br-1 - tiller_la-br-1 - deploy_la-br-1 - soak_la-br-1 + - kubernikus-dex_la-br-1 - seed_na-ca-1 - terraform_na-ca-1 - tiller_na-ca-1 - deploy_na-ca-1 - soak_na-ca-1 + - kubernikus-dex_na-ca-1 - seed_na-us-1 - terraform_na-us-1 - tiller_na-us-1 - deploy_na-us-1 - soak_na-us-1 + - kubernikus-dex_na-us-1 - seed_na-us-2 - terraform_na-us-2 - tiller_na-us-2 - deploy_na-us-2 - soak_na-us-2 + - kubernikus-dex_na-us-2 - seed_na-us-3 - terraform_na-us-3 - tiller_na-us-3 - deploy_na-us-3 - soak_na-us-3 + - kubernikus-dex_na-us-3 - seed_qa-de-1 - terraform_qa-de-1 - tiller_qa-de-1 - deploy_qa-de-1 - soak_qa-de-1 + - kubernikus-dex_qa-de-1 - name: monitoring diff --git a/ci/pipeline.yaml.erb b/ci/pipeline.yaml.erb index 304c646bf1..e7ae3d4217 100644 --- a/ci/pipeline.yaml.erb +++ b/ci/pipeline.yaml.erb @@ -455,6 +455,22 @@ jobs: <<: *task_helm_kubernikus-monitoring params: <<: *auth_<%= region %> + + - name: kubernikus-dex_<%= region %> + serial: true + plan: + - in_parallel: + - get: secrets.git + - get: kubernikus.builds + trigger: true + resource: master.builds + - in_parallel: + - task: kubernikus-dex_<%= region %> + config: + <<: *task_helm_kubernikus-dex + params: + - REGION: <%= region %> + <<: *auth_<%= region %> - name: deploy_<%= region %> serial: true @@ -562,6 +578,7 @@ groups: - tiller_<%= region %> - deploy_<%= region %> - soak_<%= region %> + - kubernikus-dex_<%= region %> <% end %> - name: monitoring diff --git a/ci/task_helm_kubernikus-dex.yaml b/ci/task_helm_kubernikus-dex.yaml new file mode 100644 index 0000000000..74f8406271 --- /dev/null +++ b/ci/task_helm_kubernikus-dex.yaml @@ -0,0 +1,49 @@ +platform: 'linux' + +image_resource: + type: registry-image + source: + repository: sapcc/kubernikus-kubectl + tag: 'v1.10.9' + +inputs: + - name: kubernikus.builds + - name: secrets.git + +run: + path: /bin/sh + args: + - -c + - | + set -exo pipefail + + # recent helm versions fails with `invalid cross-device link` when trying to rename charts.git + # rename(2) for a directory is allowed only when both the source and the destination path are on the top layer + # https://github.com/kubernetes/helm/issues/2998 + cp -a kubernikus.builds kubernikus-copy.builds + + # Dependencies are currently vendored. + # helm repo add sapcc https://charts.global.cloud.sap + helm dep up kubernikus-copy.builds/charts/kubernikus-dex/ + + helm diff upgrade kubernikus-dex kubernikus-copy.builds/charts/kubernikus-dex/ \ + --values secrets.git/$REGION/values/keystone.yaml \ + --values secrets.git/$REGION/values/global.yaml \ + --suppress-secrets --allow-unreleased --context 5 + + helm upgrade kubernikus-dex kubernikus-copy.builds/charts/kubernikus-dex/ \ + --values secrets.git/$REGION/values/keystone.yaml \ + --values secrets.git/$REGION/values/global.yaml \ + --namespace=kubernikus \ + --install + +params: + REGION: + OS_AUTH_URL: + OS_USERNAME: + OS_PASSWORD: + OS_USER_DOMAIN_NAME: + OS_PROJECT_NAME: + OS_PROJECT_DOMAIN_NAME: + KUBERNIKUS_NAME: + KUBERNIKUS_URL: diff --git a/etc/policy-ccadmin.json b/etc/policy-ccadmin.json index f2f69c251f..736f9de609 100644 --- a/etc/policy-ccadmin.json +++ b/etc/policy-ccadmin.json @@ -10,6 +10,7 @@ "TerminateCluster": "rule:kubernetes_admin", "UpdateCluster": "rule:kubernetes_admin", "GetClusterCredentials": "rule:kubernetes_user", + "GetClusterCredentialsOIDC": "rule:kubernetes_user", "GetClusterEvents": "rule:kubernetes_user", "GetClusterInfo": "rule:kubernetes_user", "GetBootstrapConfig": "rule:kubernetes_admin", diff --git a/etc/policy.json b/etc/policy.json index 5dbeca4f34..034a680d1b 100644 --- a/etc/policy.json +++ b/etc/policy.json @@ -10,6 +10,7 @@ "TerminateCluster": "rule:kubernetes_admin", "UpdateCluster": "rule:kubernetes_admin", "GetClusterCredentials": "rule:kubernetes_user", + "GetClusterCredentialsOIDC": "rule:kubernetes_user", "GetClusterEvents": "rule:kubernetes_user", "GetClusterInfo": "rule:kubernetes_user", "GetBootstrapConfig": "rule:kubernetes_admin", diff --git a/pkg/api/client/operations/get_cluster_credentials_o_id_c_parameters.go b/pkg/api/client/operations/get_cluster_credentials_o_id_c_parameters.go new file mode 100644 index 0000000000..071f6e89b3 --- /dev/null +++ b/pkg/api/client/operations/get_cluster_credentials_o_id_c_parameters.go @@ -0,0 +1,133 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "context" + "net/http" + "time" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime" + cr "github.com/go-openapi/runtime/client" + + strfmt "github.com/go-openapi/strfmt" +) + +// NewGetClusterCredentialsOIDCParams creates a new GetClusterCredentialsOIDCParams object +// with the default values initialized. +func NewGetClusterCredentialsOIDCParams() *GetClusterCredentialsOIDCParams { + var () + return &GetClusterCredentialsOIDCParams{ + + timeout: cr.DefaultTimeout, + } +} + +// NewGetClusterCredentialsOIDCParamsWithTimeout creates a new GetClusterCredentialsOIDCParams object +// with the default values initialized, and the ability to set a timeout on a request +func NewGetClusterCredentialsOIDCParamsWithTimeout(timeout time.Duration) *GetClusterCredentialsOIDCParams { + var () + return &GetClusterCredentialsOIDCParams{ + + timeout: timeout, + } +} + +// NewGetClusterCredentialsOIDCParamsWithContext creates a new GetClusterCredentialsOIDCParams object +// with the default values initialized, and the ability to set a context for a request +func NewGetClusterCredentialsOIDCParamsWithContext(ctx context.Context) *GetClusterCredentialsOIDCParams { + var () + return &GetClusterCredentialsOIDCParams{ + + Context: ctx, + } +} + +// NewGetClusterCredentialsOIDCParamsWithHTTPClient creates a new GetClusterCredentialsOIDCParams object +// with the default values initialized, and the ability to set a custom HTTPClient for a request +func NewGetClusterCredentialsOIDCParamsWithHTTPClient(client *http.Client) *GetClusterCredentialsOIDCParams { + var () + return &GetClusterCredentialsOIDCParams{ + HTTPClient: client, + } +} + +/*GetClusterCredentialsOIDCParams contains all the parameters to send to the API endpoint +for the get cluster credentials o ID c operation typically these are written to a http.Request +*/ +type GetClusterCredentialsOIDCParams struct { + + /*Name*/ + Name string + + timeout time.Duration + Context context.Context + HTTPClient *http.Client +} + +// WithTimeout adds the timeout to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) WithTimeout(timeout time.Duration) *GetClusterCredentialsOIDCParams { + o.SetTimeout(timeout) + return o +} + +// SetTimeout adds the timeout to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) SetTimeout(timeout time.Duration) { + o.timeout = timeout +} + +// WithContext adds the context to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) WithContext(ctx context.Context) *GetClusterCredentialsOIDCParams { + o.SetContext(ctx) + return o +} + +// SetContext adds the context to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) SetContext(ctx context.Context) { + o.Context = ctx +} + +// WithHTTPClient adds the HTTPClient to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) WithHTTPClient(client *http.Client) *GetClusterCredentialsOIDCParams { + o.SetHTTPClient(client) + return o +} + +// SetHTTPClient adds the HTTPClient to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) SetHTTPClient(client *http.Client) { + o.HTTPClient = client +} + +// WithName adds the name to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) WithName(name string) *GetClusterCredentialsOIDCParams { + o.SetName(name) + return o +} + +// SetName adds the name to the get cluster credentials o ID c params +func (o *GetClusterCredentialsOIDCParams) SetName(name string) { + o.Name = name +} + +// WriteToRequest writes these params to a swagger request +func (o *GetClusterCredentialsOIDCParams) WriteToRequest(r runtime.ClientRequest, reg strfmt.Registry) error { + + if err := r.SetTimeout(o.timeout); err != nil { + return err + } + var res []error + + // path param name + if err := r.SetPathParam("name", o.Name); err != nil { + return err + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} diff --git a/pkg/api/client/operations/get_cluster_credentials_o_id_c_responses.go b/pkg/api/client/operations/get_cluster_credentials_o_id_c_responses.go new file mode 100644 index 0000000000..e38cd6605c --- /dev/null +++ b/pkg/api/client/operations/get_cluster_credentials_o_id_c_responses.go @@ -0,0 +1,112 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "fmt" + "io" + + "github.com/go-openapi/runtime" + + strfmt "github.com/go-openapi/strfmt" + + "github.com/sapcc/kubernikus/pkg/api/models" +) + +// GetClusterCredentialsOIDCReader is a Reader for the GetClusterCredentialsOIDC structure. +type GetClusterCredentialsOIDCReader struct { + formats strfmt.Registry +} + +// ReadResponse reads a server response into the received o. +func (o *GetClusterCredentialsOIDCReader) ReadResponse(response runtime.ClientResponse, consumer runtime.Consumer) (interface{}, error) { + switch response.Code() { + + case 200: + result := NewGetClusterCredentialsOIDCOK() + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + return result, nil + + default: + result := NewGetClusterCredentialsOIDCDefault(response.Code()) + if err := result.readResponse(response, consumer, o.formats); err != nil { + return nil, err + } + if response.Code()/100 == 2 { + return result, nil + } + return nil, result + } +} + +// NewGetClusterCredentialsOIDCOK creates a GetClusterCredentialsOIDCOK with default headers values +func NewGetClusterCredentialsOIDCOK() *GetClusterCredentialsOIDCOK { + return &GetClusterCredentialsOIDCOK{} +} + +/*GetClusterCredentialsOIDCOK handles this case with default header values. + +OK +*/ +type GetClusterCredentialsOIDCOK struct { + Payload *models.Credentials +} + +func (o *GetClusterCredentialsOIDCOK) Error() string { + return fmt.Sprintf("[GET /api/v1/clusters/{name}/credentials/oidc][%d] getClusterCredentialsOIdCOK %+v", 200, o.Payload) +} + +func (o *GetClusterCredentialsOIDCOK) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.Credentials) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} + +// NewGetClusterCredentialsOIDCDefault creates a GetClusterCredentialsOIDCDefault with default headers values +func NewGetClusterCredentialsOIDCDefault(code int) *GetClusterCredentialsOIDCDefault { + return &GetClusterCredentialsOIDCDefault{ + _statusCode: code, + } +} + +/*GetClusterCredentialsOIDCDefault handles this case with default header values. + +Error +*/ +type GetClusterCredentialsOIDCDefault struct { + _statusCode int + + Payload *models.Error +} + +// Code gets the status code for the get cluster credentials o ID c default response +func (o *GetClusterCredentialsOIDCDefault) Code() int { + return o._statusCode +} + +func (o *GetClusterCredentialsOIDCDefault) Error() string { + return fmt.Sprintf("[GET /api/v1/clusters/{name}/credentials/oidc][%d] GetClusterCredentialsOIDC default %+v", o._statusCode, o.Payload) +} + +func (o *GetClusterCredentialsOIDCDefault) readResponse(response runtime.ClientResponse, consumer runtime.Consumer, formats strfmt.Registry) error { + + o.Payload = new(models.Error) + + // response payload + if err := consumer.Consume(response.Body(), o.Payload); err != nil && err != io.EOF { + return err + } + + return nil +} diff --git a/pkg/api/client/operations/operations_client.go b/pkg/api/client/operations/operations_client.go index c606d55f66..f41504dea9 100644 --- a/pkg/api/client/operations/operations_client.go +++ b/pkg/api/client/operations/operations_client.go @@ -111,6 +111,35 @@ func (a *Client) GetClusterCredentials(params *GetClusterCredentialsParams, auth } +/* +GetClusterCredentialsOIDC gets user specific credentials to access the cluster with o ID c +*/ +func (a *Client) GetClusterCredentialsOIDC(params *GetClusterCredentialsOIDCParams, authInfo runtime.ClientAuthInfoWriter) (*GetClusterCredentialsOIDCOK, error) { + // TODO: Validate the params before sending + if params == nil { + params = NewGetClusterCredentialsOIDCParams() + } + + result, err := a.transport.Submit(&runtime.ClientOperation{ + ID: "GetClusterCredentialsOIDC", + Method: "GET", + PathPattern: "/api/v1/clusters/{name}/credentials/oidc", + ProducesMediaTypes: []string{"application/json"}, + ConsumesMediaTypes: []string{"application/json"}, + Schemes: []string{"https"}, + Params: params, + Reader: &GetClusterCredentialsOIDCReader{formats: a.formats}, + AuthInfo: authInfo, + Context: params.Context, + Client: params.HTTPClient, + }) + if err != nil { + return nil, err + } + return result.(*GetClusterCredentialsOIDCOK), nil + +} + /* GetClusterEvents gets recent events about the cluster */ diff --git a/pkg/api/handlers/get_cluster_credentials_oidc.go b/pkg/api/handlers/get_cluster_credentials_oidc.go new file mode 100644 index 0000000000..b6271d1aa8 --- /dev/null +++ b/pkg/api/handlers/get_cluster_credentials_oidc.go @@ -0,0 +1,81 @@ +package handlers + +import ( + "fmt" + + "github.com/ghodss/yaml" + "github.com/go-openapi/runtime/middleware" + apierrors "k8s.io/apimachinery/pkg/api/errors" + meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + "github.com/sapcc/kubernikus/pkg/api" + "github.com/sapcc/kubernikus/pkg/api/models" + "github.com/sapcc/kubernikus/pkg/api/rest/operations" + "github.com/sapcc/kubernikus/pkg/client/kubernetes" + "github.com/sapcc/kubernikus/pkg/util" +) + +func NewGetClusterCredentialsOIDC(rt *api.Runtime) operations.GetClusterCredentialsOIDCHandler { + return &getClusterCredentialsOIDC{rt} +} + +type getClusterCredentialsOIDC struct { + *api.Runtime +} + +func (d *getClusterCredentialsOIDC) Handle(params operations.GetClusterCredentialsOIDCParams, principal *models.Principal) middleware.Responder { + + kluster, err := d.Klusters.Klusters(d.Namespace).Get(qualifiedName(params.Name, principal.Account)) + if err != nil { + if apierrors.IsNotFound(err) { + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 404, "Kluster not found") + } + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 500, err.Error()) + } + + if !kluster.Spec.Dex { + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 404, "Dex not enabled, no OIDC credentials") + } + + secret, err := util.KlusterSecret(d.Kubernetes, kluster) + if err != nil { + if apierrors.IsNotFound(err) { + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 404, "Secret not found") + } + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 500, err.Error()) + } + + ingress, err := d.Runtime.Kubernetes.ExtensionsV1beta1().Ingresses(d.Namespace).Get(fmt.Sprintf("%s-dex", kluster.Name), meta_v1.GetOptions{}) + if err != nil { + if apierrors.IsNotFound(err) { + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 404, "Ingress not found") + } + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 500, err.Error()) + } + idpHost := "" + if len(ingress.Spec.Rules) == 0 { + return NewErrorResponse(&operations.GetClusterCredentialsDefault{}, 500, "no rule found in ingress") + } else { + idpHost = ingress.Spec.Rules[0].Host + } + + config := kubernetes.NewClientConfigV1OIDC( + params.Name, + fmt.Sprintf("oidc@%v", params.Name), + kluster.Status.Apiserver, + secret.DexClientSecret, + fmt.Sprintf("https://%s", idpHost), + []byte(secret.TLSCACertificate), + ) + + kubeconfig, err := yaml.Marshal(config) + if err != nil { + return NewErrorResponse(&operations.GetClusterCredentialsOIDCDefault{}, 500, "Failed to generate YAML document: %s", err) + } + + credentials := models.Credentials{ + Kubeconfig: string(kubeconfig), + } + + return operations.NewGetClusterCredentialsOIDCOK().WithPayload(&credentials) +} diff --git a/pkg/api/handlers/update_cluster.go b/pkg/api/handlers/update_cluster.go index 24bc084786..d58a9f2dc9 100644 --- a/pkg/api/handlers/update_cluster.go +++ b/pkg/api/handlers/update_cluster.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "strings" "github.com/Masterminds/semver" "github.com/go-openapi/runtime/middleware" @@ -140,6 +141,38 @@ func (d *updateCluster) Handle(params operations.UpdateClusterParams, principal } + // If dex is disabled + if !kluster.Spec.Dex { + + // Check for dashboard + if params.Body.Spec.Dashboard && !params.Body.Spec.Dex { + return apierrors.NewBadRequest(fmt.Sprintf("Dashboard cannot be enabled while Dex is disabled")) + } + + // Enable dex + if params.Body.Spec.Dex { + kluster.Spec.Dex = params.Body.Spec.Dex + } + + // Enable dashboard + if params.Body.Spec.Dashboard { + kluster.Spec.Dashboard = params.Body.Spec.Dashboard + if kluster.Status.Apiserver != "" { + apiURL := kluster.Status.Apiserver + kluster.Status.Dashboard = strings.Replace(apiURL, kluster.GetName(), fmt.Sprintf("dashboard-%s.ingress", kluster.GetName()), -1) + } + } + } else { + // Enable dashboard if dex is enabled + if params.Body.Spec.Dashboard { + kluster.Spec.Dashboard = params.Body.Spec.Dashboard + if kluster.Status.Apiserver != "" { + apiURL := kluster.Status.Apiserver + kluster.Status.Dashboard = strings.Replace(apiURL, kluster.GetName(), fmt.Sprintf("dashboard-%s.ingress", kluster.GetName()), -1) + } + } + } + return nil }) diff --git a/pkg/api/models/kluster_spec.go b/pkg/api/models/kluster_spec.go index 83c2bfc40b..6fb90fdda5 100644 --- a/pkg/api/models/kluster_spec.go +++ b/pkg/api/models/kluster_spec.go @@ -31,6 +31,12 @@ type KlusterSpec struct { // Pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\/([0-9]|[1-2][0-9]|3[0-2]))$ ClusterCIDR string `json:"clusterCIDR,omitempty"` + // dashboard + Dashboard bool `json:"dashboard,omitempty"` + + // dex + Dex bool `json:"dex,omitempty"` + // dns address // Pattern: ^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$ DNSAddress string `json:"dnsAddress,omitempty"` diff --git a/pkg/api/models/kluster_status.go b/pkg/api/models/kluster_status.go index 98e01be850..edcb378796 100644 --- a/pkg/api/models/kluster_status.go +++ b/pkg/api/models/kluster_status.go @@ -30,6 +30,9 @@ type KlusterStatus struct { // chart version ChartVersion string `json:"chartVersion,omitempty"` + // dashboard + Dashboard string `json:"dashboard,omitempty"` + // migrations pending MigrationsPending bool `json:"migrationsPending,omitempty"` diff --git a/pkg/api/rest/configure.go b/pkg/api/rest/configure.go index 2725028897..87cb48569f 100644 --- a/pkg/api/rest/configure.go +++ b/pkg/api/rest/configure.go @@ -61,6 +61,7 @@ func Configure(api *operations.KubernikusAPI, rt *apipkg.Runtime) error { api.TerminateClusterHandler = handlers.NewTerminateCluster(rt) api.UpdateClusterHandler = handlers.NewUpdateCluster(rt) api.GetClusterCredentialsHandler = handlers.NewGetClusterCredentials(rt) + api.GetClusterCredentialsOIDCHandler = handlers.NewGetClusterCredentialsOIDC(rt) api.GetClusterInfoHandler = handlers.NewGetClusterInfo(rt) api.GetBootstrapConfigHandler = handlers.NewGetBootstrapConfig(rt) api.GetOpenstackMetadataHandler = handlers.NewGetOpenstackMetadata(rt) diff --git a/pkg/api/rest/embedded_spec.go b/pkg/api/rest/embedded_spec.go new file mode 100644 index 0000000000..b5001de2b5 --- /dev/null +++ b/pkg/api/rest/embedded_spec.go @@ -0,0 +1,1973 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package rest + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "encoding/json" +) + +var ( + // SwaggerJSON embedded version of the swagger document used at generation time + SwaggerJSON json.RawMessage + // FlatSwaggerJSON embedded flattened version of the swagger document used at generation time + FlatSwaggerJSON json.RawMessage +) + +func init() { + SwaggerJSON = json.RawMessage([]byte(`{ + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "swagger": "2.0", + "info": { + "title": "Kubernikus", + "version": "1.0.0" + }, + "paths": { + "/api": { + "get": { + "security": [], + "summary": "List available api versions", + "operationId": "ListAPIVersions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ApiVersions" + } + } + } + } + }, + "/api/v1/clusters": { + "get": { + "summary": "List available clusters", + "operationId": "ListClusters", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Kluster" + } + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "post": { + "summary": "Create a cluster", + "operationId": "CreateCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Kluster" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + } + }, + "/api/v1/clusters/{name}": { + "get": { + "summary": "Show the specified cluser", + "operationId": "ShowCluster", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "put": { + "summary": "Update the specified cluser", + "operationId": "UpdateCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Kluster" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "delete": { + "summary": "Terminate the specified cluster", + "operationId": "TerminateCluster", + "responses": { + "202": { + "description": "OK" + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/bootstrap": { + "get": { + "summary": "Get bootstrap config to onboard a node", + "operationId": "GetBootstrapConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/BootstrapConfig" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/credentials": { + "get": { + "summary": "Get user specific credentials to access the cluster", + "operationId": "GetClusterCredentials", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/credentials/oidc": { + "get": { + "summary": "Get user specific credentials to access the cluster with OIDC", + "operationId": "GetClusterCredentialsOIDC", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/events": { + "get": { + "summary": "Get recent events about the cluster", + "operationId": "GetClusterEvents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/info": { + "get": { + "summary": "Get user specific info about the cluster", + "operationId": "GetClusterInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/KlusterInfo" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/openstack/metadata": { + "get": { + "summary": "Grab bag of openstack metadata", + "operationId": "GetOpenstackMetadata", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OpenstackMetadata" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + } + }, + "/api/v1/{account}/clusters/{name}/values": { + "get": { + "summary": "Get values for cluster chart (admin-only)", + "operationId": "GetClusterValues", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "object", + "properties": { + "values": { + "description": "The values in yaml Format", + "type": "string" + } + } + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "uniqueItems": true, + "type": "string", + "name": "account", + "in": "path", + "required": true + } + ] + }, + "/info": { + "get": { + "security": [], + "summary": "Get info about Kubernikus", + "operationId": "Info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Info" + } + } + } + } + } + }, + "definitions": { + "ApiVersions": { + "required": [ + "versions" + ], + "properties": { + "versions": { + "description": "versions are the api versions that are available.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BootstrapConfig": { + "type": "object", + "properties": { + "config": { + "type": "string" + }, + "kubeconfig": { + "type": "string" + }, + "kubeletClientsCA": { + "type": "string" + }, + "kubeletClientsCAFile": { + "type": "string" + } + } + }, + "Credentials": { + "type": "object", + "properties": { + "kubeconfig": { + "type": "string" + } + } + }, + "Event": { + "type": "object", + "properties": { + "count": { + "description": "The number of times this event has occurred.", + "type": "integer" + }, + "firstTimestamp": { + "description": "The time at which the event was first recorded", + "type": "string" + }, + "lastTimestamp": { + "description": "The time at which the most recent occurrence of this event was recorded", + "type": "string" + }, + "message": { + "description": "A human-readable description of the event", + "type": "string" + }, + "reason": { + "description": "A short, machine understandable string that gives the reason for the event", + "type": "string" + }, + "type": { + "description": "Type of this event", + "type": "string", + "enum": [ + "Normal", + "Warning" + ] + } + } + }, + "Info": { + "properties": { + "availableClusterVersions": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + }, + "defaultClusterVersion": { + "type": "string" + }, + "gitVersion": { + "type": "string" + }, + "supportedClusterVersions": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + } + } + }, + "Kluster": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "name of the cluster", + "type": "string", + "maxLength": 20, + "pattern": "^[a-z]([-a-z0-9]*[a-z0-9])?$", + "x-nullable": false + }, + "spec": { + "$ref": "#/definitions/KlusterSpec" + }, + "status": { + "$ref": "#/definitions/KlusterStatus" + } + } + }, + "KlusterInfo": { + "properties": { + "binaries": { + "type": "array", + "items": { + "type": "object", + "properties": { + "links": { + "type": "array", + "items": { + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "platform": { + "type": "string" + } + }, + "x-go-name": "Link", + "x-nullable": false + } + }, + "name": { + "type": "string" + } + }, + "x-go-name": "Binaries", + "x-nullable": false + } + }, + "setupCommand": { + "type": "string" + } + } + }, + "KlusterPhase": { + "type": "string", + "enum": [ + "Pending", + "Creating", + "Running", + "Upgrading", + "Terminating" + ] + }, + "KlusterSpec": { + "type": "object", + "properties": { + "advertiseAddress": { + "type": "string", + "default": "1.1.1.1", + "x-nullable": false + }, + "clusterCIDR": { + "description": "CIDR Range for Pods in the cluster. Can not be updated.", + "type": "string", + "default": "100.100.0.0/16", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", + "x-nullable": false + }, + "dashboard": { + "type": "boolean" + }, + "dex": { + "type": "boolean" + }, + "dnsAddress": { + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + }, + "dnsDomain": { + "type": "string", + "default": "cluster.local", + "x-nullable": false + }, + "name": { + "type": "string", + "readOnly": true + }, + "noCloud": { + "type": "boolean" + }, + "nodePools": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePool" + } + }, + "openstack": { + "$ref": "#/definitions/OpenstackSpec" + }, + "serviceCIDR": { + "description": "CIDR Range for Services in the cluster. Can not be updated.", + "type": "string", + "default": "198.18.128.0/17", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", + "x-nullable": false + }, + "sshPublicKey": { + "description": "SSH public key that is injected into spawned nodes.", + "type": "string" + }, + "version": { + "description": "Kubernetes version", + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + } + }, + "x-nullable": false + }, + "KlusterStatus": { + "type": "object", + "properties": { + "apiserver": { + "type": "string" + }, + "apiserverVersion": { + "type": "string" + }, + "chartName": { + "type": "string" + }, + "chartVersion": { + "type": "string" + }, + "dashboard": { + "type": "string" + }, + "migrationsPending": { + "type": "boolean" + }, + "nodePools": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePoolInfo" + } + }, + "phase": { + "$ref": "#/definitions/KlusterPhase" + }, + "specVersion": { + "type": "integer" + }, + "version": { + "type": "string" + }, + "wormhole": { + "type": "string" + } + }, + "x-nullable": false, + "readOnly": true + }, + "NodePool": { + "type": "object", + "required": [ + "name", + "flavor" + ], + "properties": { + "availabilityZone": { + "type": "string", + "x-nullable": false + }, + "config": { + "$ref": "#/definitions/NodePoolConfig" + }, + "flavor": { + "type": "string", + "x-nullable": false + }, + "image": { + "type": "string", + "default": "coreos-stable-amd64", + "x-nullable": false + }, + "labels": { + "description": "The specified labels will be added to members of this pool once during initial registration of the node", + "type": "array", + "items": { + "type": "string", + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])(\\.[a-z0-9]([-a-z0-9]*[a-z0-9]))*/)?[A-Za-z0-9][-A-Za-z0-9_.]{0,62}=[A-Za-z0-9][-A-Za-z0-9_.]{0,62}$" + } + }, + "name": { + "type": "string", + "maxLength": 20, + "pattern": "^[a-z0-9]([-\\.a-z0-9]*)?$", + "x-nullable": false + }, + "size": { + "type": "integer", + "default": 0, + "maximum": 127, + "x-nullable": false + }, + "taints": { + "description": "The specified taints will be added to members of this pool once during initial registration of the node", + "type": "array", + "items": { + "type": "string", + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])(\\.[a-z0-9]([-a-z0-9]*[a-z0-9]))*/)?[A-Za-z0-9][-A-Za-z0-9_.]{0,62}=[A-Za-z0-9][-A-Za-z0-9_.]{0,62}:(NoSchedule|NoExecute|PreferNoSchedule)$" + } + } + }, + "x-nullable": false + }, + "NodePoolConfig": { + "type": "object", + "properties": { + "allowReboot": { + "description": "Allow automatic drain and reboot of nodes. Enables OS updates. Required by security policy.", + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, + "allowReplace": { + "description": "Allow automatic drain and replacement of nodes. Enables Kubernetes upgrades.", + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + } + }, + "x-nullable": true + }, + "NodePoolInfo": { + "type": "object", + "properties": { + "healthy": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "running": { + "type": "integer" + }, + "schedulable": { + "type": "integer" + }, + "size": { + "type": "integer" + } + }, + "x-nullable": false + }, + "OpenstackMetadata": { + "type": "object", + "properties": { + "availabilityZones": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "x-go-name": "AvailabilityZone", + "x-nullable": false + } + }, + "flavors": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ram": { + "type": "integer" + }, + "vcpus": { + "type": "integer" + } + }, + "x-go-name": "Flavor", + "x-nullable": false + } + }, + "keyPairs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + }, + "x-go-name": "KeyPair" + } + }, + "routers": { + "type": "array", + "items": { + "type": "object", + "properties": { + "externalNetworkID": { + "type": "string", + "x-go-name": "ExternalNetworkID" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "networks": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "subnets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "CIDR": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "x-go-name": "Subnet" + } + } + }, + "x-go-name": "Network" + } + } + }, + "x-go-name": "Router" + } + }, + "securityGroups": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "x-go-name": "SecurityGroup" + } + } + } + }, + "OpenstackSpec": { + "type": "object", + "properties": { + "lbFloatingNetworkID": { + "type": "string", + "x-go-name": "LBFloatingNetworkID" + }, + "lbSubnetID": { + "type": "string", + "x-go-name": "LBSubnetID" + }, + "networkID": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "routerID": { + "type": "string" + }, + "securityGroupName": { + "type": "string" + } + }, + "x-nullable": false + }, + "Principal": { + "type": "object", + "properties": { + "account": { + "description": "account id", + "type": "string" + }, + "account_name": { + "description": "account name", + "type": "string" + }, + "authUrl": { + "description": "Identity Endpoint", + "type": "string" + }, + "domain": { + "description": "user's domain name", + "type": "string" + }, + "id": { + "description": "userid", + "type": "string" + }, + "name": { + "description": "username", + "type": "string" + }, + "roles": { + "description": "list of roles the user has in the given scope", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "error": { + "description": "the error model is a model for all the error responses coming from Kubernikus\n", + "type": "object", + "required": [ + "message", + "code" + ], + "properties": { + "code": { + "description": "The error code", + "type": "integer", + "x-nullable": false + }, + "helpUrl": { + "description": "link to help page explaining the error in more detail", + "type": "string", + "format": "uri" + }, + "message": { + "description": "The error message", + "type": "string", + "x-nullable": false + } + } + } + }, + "responses": { + "errorResponse": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + }, + "securityDefinitions": { + "keystone": { + "description": "OpenStack Keystone Authentication", + "type": "apiKey", + "name": "x-auth-token", + "in": "header" + } + }, + "security": [ + { + "keystone": null + } + ] +}`)) + FlatSwaggerJSON = json.RawMessage([]byte(`{ + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "swagger": "2.0", + "info": { + "title": "Kubernikus", + "version": "1.0.0" + }, + "paths": { + "/api": { + "get": { + "security": [], + "summary": "List available api versions", + "operationId": "ListAPIVersions", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/ApiVersions" + } + } + } + } + }, + "/api/v1/clusters": { + "get": { + "summary": "List available clusters", + "operationId": "ListClusters", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Kluster" + } + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "post": { + "summary": "Create a cluster", + "operationId": "CreateCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Kluster" + } + } + ], + "responses": { + "201": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/api/v1/clusters/{name}": { + "get": { + "summary": "Show the specified cluser", + "operationId": "ShowCluster", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "put": { + "summary": "Update the specified cluser", + "operationId": "UpdateCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Kluster" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Kluster" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "delete": { + "summary": "Terminate the specified cluster", + "operationId": "TerminateCluster", + "responses": { + "202": { + "description": "OK" + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/bootstrap": { + "get": { + "summary": "Get bootstrap config to onboard a node", + "operationId": "GetBootstrapConfig", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/BootstrapConfig" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/credentials": { + "get": { + "summary": "Get user specific credentials to access the cluster", + "operationId": "GetClusterCredentials", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/credentials/oidc": { + "get": { + "summary": "Get user specific credentials to access the cluster with OIDC", + "operationId": "GetClusterCredentialsOIDC", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/events": { + "get": { + "summary": "Get recent events about the cluster", + "operationId": "GetClusterEvents", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Event" + } + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/clusters/{name}/info": { + "get": { + "summary": "Get user specific info about the cluster", + "operationId": "GetClusterInfo", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/KlusterInfo" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, + "/api/v1/openstack/metadata": { + "get": { + "summary": "Grab bag of openstack metadata", + "operationId": "GetOpenstackMetadata", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/OpenstackMetadata" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + } + }, + "/api/v1/{account}/clusters/{name}/values": { + "get": { + "summary": "Get values for cluster chart (admin-only)", + "operationId": "GetClusterValues", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/getClusterValuesOKBody" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + }, + { + "uniqueItems": true, + "type": "string", + "name": "account", + "in": "path", + "required": true + } + ] + }, + "/info": { + "get": { + "security": [], + "summary": "Get info about Kubernikus", + "operationId": "Info", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Info" + } + } + } + } + } + }, + "definitions": { + "ApiVersions": { + "required": [ + "versions" + ], + "properties": { + "versions": { + "description": "versions are the api versions that are available.", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "BootstrapConfig": { + "type": "object", + "properties": { + "config": { + "type": "string" + }, + "kubeconfig": { + "type": "string" + }, + "kubeletClientsCA": { + "type": "string" + }, + "kubeletClientsCAFile": { + "type": "string" + } + } + }, + "Credentials": { + "type": "object", + "properties": { + "kubeconfig": { + "type": "string" + } + } + }, + "Event": { + "type": "object", + "properties": { + "count": { + "description": "The number of times this event has occurred.", + "type": "integer" + }, + "firstTimestamp": { + "description": "The time at which the event was first recorded", + "type": "string" + }, + "lastTimestamp": { + "description": "The time at which the most recent occurrence of this event was recorded", + "type": "string" + }, + "message": { + "description": "A human-readable description of the event", + "type": "string" + }, + "reason": { + "description": "A short, machine understandable string that gives the reason for the event", + "type": "string" + }, + "type": { + "description": "Type of this event", + "type": "string", + "enum": [ + "Normal", + "Warning" + ] + } + } + }, + "Info": { + "properties": { + "availableClusterVersions": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + }, + "defaultClusterVersion": { + "type": "string" + }, + "gitVersion": { + "type": "string" + }, + "supportedClusterVersions": { + "type": "array", + "items": { + "type": "string" + }, + "x-omitempty": true + } + } + }, + "Kluster": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "description": "name of the cluster", + "type": "string", + "maxLength": 20, + "pattern": "^[a-z]([-a-z0-9]*[a-z0-9])?$", + "x-nullable": false + }, + "spec": { + "$ref": "#/definitions/KlusterSpec" + }, + "status": { + "$ref": "#/definitions/KlusterStatus" + } + } + }, + "KlusterInfo": { + "properties": { + "binaries": { + "type": "array", + "items": { + "$ref": "#/definitions/klusterInfoBinariesItems" + } + }, + "setupCommand": { + "type": "string" + } + } + }, + "KlusterPhase": { + "type": "string", + "enum": [ + "Pending", + "Creating", + "Running", + "Upgrading", + "Terminating" + ] + }, + "KlusterSpec": { + "type": "object", + "properties": { + "advertiseAddress": { + "type": "string", + "default": "1.1.1.1", + "x-nullable": false + }, + "clusterCIDR": { + "description": "CIDR Range for Pods in the cluster. Can not be updated.", + "type": "string", + "default": "100.100.0.0/16", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", + "x-nullable": false + }, + "dashboard": { + "type": "boolean" + }, + "dex": { + "type": "boolean" + }, + "dnsAddress": { + "type": "string", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" + }, + "dnsDomain": { + "type": "string", + "default": "cluster.local", + "x-nullable": false + }, + "name": { + "type": "string", + "readOnly": true + }, + "noCloud": { + "type": "boolean" + }, + "nodePools": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePool" + } + }, + "openstack": { + "$ref": "#/definitions/OpenstackSpec" + }, + "serviceCIDR": { + "description": "CIDR Range for Services in the cluster. Can not be updated.", + "type": "string", + "default": "198.18.128.0/17", + "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", + "x-nullable": false + }, + "sshPublicKey": { + "description": "SSH public key that is injected into spawned nodes.", + "type": "string" + }, + "version": { + "description": "Kubernetes version", + "type": "string", + "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+$" + } + }, + "x-nullable": false + }, + "KlusterStatus": { + "type": "object", + "properties": { + "apiserver": { + "type": "string" + }, + "apiserverVersion": { + "type": "string" + }, + "chartName": { + "type": "string" + }, + "chartVersion": { + "type": "string" + }, + "dashboard": { + "type": "string" + }, + "migrationsPending": { + "type": "boolean" + }, + "nodePools": { + "type": "array", + "items": { + "$ref": "#/definitions/NodePoolInfo" + } + }, + "phase": { + "$ref": "#/definitions/KlusterPhase" + }, + "specVersion": { + "type": "integer" + }, + "version": { + "type": "string" + }, + "wormhole": { + "type": "string" + } + }, + "x-nullable": false, + "readOnly": true + }, + "NodePool": { + "type": "object", + "required": [ + "name", + "flavor" + ], + "properties": { + "availabilityZone": { + "type": "string", + "x-nullable": false + }, + "config": { + "$ref": "#/definitions/NodePoolConfig" + }, + "flavor": { + "type": "string", + "x-nullable": false + }, + "image": { + "type": "string", + "default": "coreos-stable-amd64", + "x-nullable": false + }, + "labels": { + "description": "The specified labels will be added to members of this pool once during initial registration of the node", + "type": "array", + "items": { + "type": "string", + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])(\\.[a-z0-9]([-a-z0-9]*[a-z0-9]))*/)?[A-Za-z0-9][-A-Za-z0-9_.]{0,62}=[A-Za-z0-9][-A-Za-z0-9_.]{0,62}$" + } + }, + "name": { + "type": "string", + "maxLength": 20, + "pattern": "^[a-z0-9]([-\\.a-z0-9]*)?$", + "x-nullable": false + }, + "size": { + "type": "integer", + "default": 0, + "maximum": 127, + "minimum": 0, + "x-nullable": false + }, + "taints": { + "description": "The specified taints will be added to members of this pool once during initial registration of the node", + "type": "array", + "items": { + "type": "string", + "pattern": "^([a-z0-9]([-a-z0-9]*[a-z0-9])(\\.[a-z0-9]([-a-z0-9]*[a-z0-9]))*/)?[A-Za-z0-9][-A-Za-z0-9_.]{0,62}=[A-Za-z0-9][-A-Za-z0-9_.]{0,62}:(NoSchedule|NoExecute|PreferNoSchedule)$" + } + } + }, + "x-nullable": false + }, + "NodePoolConfig": { + "type": "object", + "properties": { + "allowReboot": { + "description": "Allow automatic drain and reboot of nodes. Enables OS updates. Required by security policy.", + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + }, + "allowReplace": { + "description": "Allow automatic drain and replacement of nodes. Enables Kubernetes upgrades.", + "type": "boolean", + "x-nullable": true, + "x-omitempty": false + } + }, + "x-nullable": true + }, + "NodePoolInfo": { + "type": "object", + "properties": { + "healthy": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "running": { + "type": "integer" + }, + "schedulable": { + "type": "integer" + }, + "size": { + "type": "integer" + } + }, + "x-nullable": false + }, + "OpenstackMetadata": { + "type": "object", + "properties": { + "availabilityZones": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataAvailabilityZonesItems" + } + }, + "flavors": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataFlavorsItems" + } + }, + "keyPairs": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataKeyPairsItems" + } + }, + "routers": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataRoutersItems" + } + }, + "securityGroups": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataSecurityGroupsItems" + } + } + } + }, + "OpenstackSpec": { + "type": "object", + "properties": { + "lbFloatingNetworkID": { + "type": "string", + "x-go-name": "LBFloatingNetworkID" + }, + "lbSubnetID": { + "type": "string", + "x-go-name": "LBSubnetID" + }, + "networkID": { + "type": "string" + }, + "projectID": { + "type": "string" + }, + "routerID": { + "type": "string" + }, + "securityGroupName": { + "type": "string" + } + }, + "x-nullable": false + }, + "Principal": { + "type": "object", + "properties": { + "account": { + "description": "account id", + "type": "string" + }, + "account_name": { + "description": "account name", + "type": "string" + }, + "authUrl": { + "description": "Identity Endpoint", + "type": "string" + }, + "domain": { + "description": "user's domain name", + "type": "string" + }, + "id": { + "description": "userid", + "type": "string" + }, + "name": { + "description": "username", + "type": "string" + }, + "roles": { + "description": "list of roles the user has in the given scope", + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "error": { + "description": "the error model is a model for all the error responses coming from Kubernikus\n", + "type": "object", + "required": [ + "message", + "code" + ], + "properties": { + "code": { + "description": "The error code", + "type": "integer", + "x-nullable": false + }, + "helpUrl": { + "description": "link to help page explaining the error in more detail", + "type": "string", + "format": "uri" + }, + "message": { + "description": "The error message", + "type": "string", + "x-nullable": false + } + } + }, + "getClusterValuesOKBody": { + "type": "object", + "properties": { + "values": { + "description": "The values in yaml Format", + "type": "string" + } + }, + "x-go-gen-location": "operations" + }, + "klusterInfoBinariesItems": { + "type": "object", + "properties": { + "links": { + "type": "array", + "items": { + "$ref": "#/definitions/klusterInfoBinariesItemsLinksItems" + } + }, + "name": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "Binaries", + "x-nullable": false + }, + "klusterInfoBinariesItemsLinksItems": { + "type": "object", + "properties": { + "link": { + "type": "string" + }, + "platform": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "Link", + "x-nullable": false + }, + "openstackMetadataAvailabilityZonesItems": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "AvailabilityZone", + "x-nullable": false + }, + "openstackMetadataFlavorsItems": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "ram": { + "type": "integer" + }, + "vcpus": { + "type": "integer" + } + }, + "x-go-gen-location": "models", + "x-go-name": "Flavor", + "x-nullable": false + }, + "openstackMetadataKeyPairsItems": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "publicKey": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "KeyPair" + }, + "openstackMetadataRoutersItems": { + "type": "object", + "properties": { + "externalNetworkID": { + "type": "string", + "x-go-name": "ExternalNetworkID" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "networks": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataRoutersItemsNetworksItems" + } + } + }, + "x-go-gen-location": "models", + "x-go-name": "Router" + }, + "openstackMetadataRoutersItemsNetworksItems": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "subnets": { + "type": "array", + "items": { + "$ref": "#/definitions/openstackMetadataRoutersItemsNetworksItemsSubnetsItems" + } + } + }, + "x-go-gen-location": "models", + "x-go-name": "Network" + }, + "openstackMetadataRoutersItemsNetworksItemsSubnetsItems": { + "type": "object", + "properties": { + "CIDR": { + "type": "string" + }, + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "Subnet" + }, + "openstackMetadataSecurityGroupsItems": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "x-go-gen-location": "models", + "x-go-name": "SecurityGroup" + } + }, + "responses": { + "errorResponse": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + }, + "securityDefinitions": { + "keystone": { + "description": "OpenStack Keystone Authentication", + "type": "apiKey", + "name": "x-auth-token", + "in": "header" + } + }, + "security": [ + { + "keystone": [] + } + ] +}`)) +} diff --git a/pkg/api/rest/operations/get_cluster_credentials_o_id_c.go b/pkg/api/rest/operations/get_cluster_credentials_o_id_c.go new file mode 100644 index 0000000000..6a4b62a2cc --- /dev/null +++ b/pkg/api/rest/operations/get_cluster_credentials_o_id_c.go @@ -0,0 +1,73 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "net/http" + + middleware "github.com/go-openapi/runtime/middleware" + + models "github.com/sapcc/kubernikus/pkg/api/models" +) + +// GetClusterCredentialsOIDCHandlerFunc turns a function with the right signature into a get cluster credentials o ID c handler +type GetClusterCredentialsOIDCHandlerFunc func(GetClusterCredentialsOIDCParams, *models.Principal) middleware.Responder + +// Handle executing the request and returning a response +func (fn GetClusterCredentialsOIDCHandlerFunc) Handle(params GetClusterCredentialsOIDCParams, principal *models.Principal) middleware.Responder { + return fn(params, principal) +} + +// GetClusterCredentialsOIDCHandler interface for that can handle valid get cluster credentials o ID c params +type GetClusterCredentialsOIDCHandler interface { + Handle(GetClusterCredentialsOIDCParams, *models.Principal) middleware.Responder +} + +// NewGetClusterCredentialsOIDC creates a new http.Handler for the get cluster credentials o ID c operation +func NewGetClusterCredentialsOIDC(ctx *middleware.Context, handler GetClusterCredentialsOIDCHandler) *GetClusterCredentialsOIDC { + return &GetClusterCredentialsOIDC{Context: ctx, Handler: handler} +} + +/*GetClusterCredentialsOIDC swagger:route GET /api/v1/clusters/{name}/credentials/oidc getClusterCredentialsOIdC + +Get user specific credentials to access the cluster with OIDC + +*/ +type GetClusterCredentialsOIDC struct { + Context *middleware.Context + Handler GetClusterCredentialsOIDCHandler +} + +func (o *GetClusterCredentialsOIDC) ServeHTTP(rw http.ResponseWriter, r *http.Request) { + route, rCtx, _ := o.Context.RouteInfo(r) + if rCtx != nil { + r = rCtx + } + var Params = NewGetClusterCredentialsOIDCParams() + + uprinc, aCtx, err := o.Context.Authorize(r, route) + if err != nil { + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + if aCtx != nil { + r = aCtx + } + var principal *models.Principal + if uprinc != nil { + principal = uprinc.(*models.Principal) // this is really a models.Principal, I promise + } + + if err := o.Context.BindValidRequest(r, route, &Params); err != nil { // bind params + o.Context.Respond(rw, r, route.Produces, route, err) + return + } + + res := o.Handler.Handle(Params, principal) // actually handle the request + + o.Context.Respond(rw, r, route.Produces, route, res) + +} diff --git a/pkg/api/rest/operations/get_cluster_credentials_o_id_c_parameters.go b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_parameters.go new file mode 100644 index 0000000000..6a99a65824 --- /dev/null +++ b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_parameters.go @@ -0,0 +1,84 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/errors" + "github.com/go-openapi/runtime/middleware" + + strfmt "github.com/go-openapi/strfmt" +) + +// NewGetClusterCredentialsOIDCParams creates a new GetClusterCredentialsOIDCParams object +// no default values defined in spec. +func NewGetClusterCredentialsOIDCParams() GetClusterCredentialsOIDCParams { + + return GetClusterCredentialsOIDCParams{} +} + +// GetClusterCredentialsOIDCParams contains all the bound params for the get cluster credentials o ID c operation +// typically these are obtained from a http.Request +// +// swagger:parameters GetClusterCredentialsOIDC +type GetClusterCredentialsOIDCParams struct { + + // HTTP Request Object + HTTPRequest *http.Request `json:"-"` + + /* + Required: true + Unique: true + In: path + */ + Name string +} + +// BindRequest both binds and validates a request, it assumes that complex things implement a Validatable(strfmt.Registry) error interface +// for simple values it will use straight method calls. +// +// To ensure default values, the struct must have been initialized with NewGetClusterCredentialsOIDCParams() beforehand. +func (o *GetClusterCredentialsOIDCParams) BindRequest(r *http.Request, route *middleware.MatchedRoute) error { + var res []error + + o.HTTPRequest = r + + rName, rhkName, _ := route.Params.GetOK("name") + if err := o.bindName(rName, rhkName, route.Formats); err != nil { + res = append(res, err) + } + + if len(res) > 0 { + return errors.CompositeValidationError(res...) + } + return nil +} + +// bindName binds and validates parameter Name from path. +func (o *GetClusterCredentialsOIDCParams) bindName(rawData []string, hasKey bool, formats strfmt.Registry) error { + var raw string + if len(rawData) > 0 { + raw = rawData[len(rawData)-1] + } + + // Required: true + // Parameter is provided by construction from the route + + o.Name = raw + + if err := o.validateName(formats); err != nil { + return err + } + + return nil +} + +// validateName carries on validations for parameter Name +func (o *GetClusterCredentialsOIDCParams) validateName(formats strfmt.Registry) error { + + return nil +} diff --git a/pkg/api/rest/operations/get_cluster_credentials_o_id_c_responses.go b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_responses.go new file mode 100644 index 0000000000..865f241836 --- /dev/null +++ b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_responses.go @@ -0,0 +1,116 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the swagger generate command + +import ( + "net/http" + + "github.com/go-openapi/runtime" + + models "github.com/sapcc/kubernikus/pkg/api/models" +) + +// GetClusterCredentialsOIDCOKCode is the HTTP code returned for type GetClusterCredentialsOIDCOK +const GetClusterCredentialsOIDCOKCode int = 200 + +/*GetClusterCredentialsOIDCOK OK + +swagger:response getClusterCredentialsOIdCOK +*/ +type GetClusterCredentialsOIDCOK struct { + + /* + In: Body + */ + Payload *models.Credentials `json:"body,omitempty"` +} + +// NewGetClusterCredentialsOIDCOK creates GetClusterCredentialsOIDCOK with default headers values +func NewGetClusterCredentialsOIDCOK() *GetClusterCredentialsOIDCOK { + + return &GetClusterCredentialsOIDCOK{} +} + +// WithPayload adds the payload to the get cluster credentials o Id c o k response +func (o *GetClusterCredentialsOIDCOK) WithPayload(payload *models.Credentials) *GetClusterCredentialsOIDCOK { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get cluster credentials o Id c o k response +func (o *GetClusterCredentialsOIDCOK) SetPayload(payload *models.Credentials) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetClusterCredentialsOIDCOK) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(200) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} + +/*GetClusterCredentialsOIDCDefault Error + +swagger:response getClusterCredentialsOIdCDefault +*/ +type GetClusterCredentialsOIDCDefault struct { + _statusCode int + + /* + In: Body + */ + Payload *models.Error `json:"body,omitempty"` +} + +// NewGetClusterCredentialsOIDCDefault creates GetClusterCredentialsOIDCDefault with default headers values +func NewGetClusterCredentialsOIDCDefault(code int) *GetClusterCredentialsOIDCDefault { + if code <= 0 { + code = 500 + } + + return &GetClusterCredentialsOIDCDefault{ + _statusCode: code, + } +} + +// WithStatusCode adds the status to the get cluster credentials o ID c default response +func (o *GetClusterCredentialsOIDCDefault) WithStatusCode(code int) *GetClusterCredentialsOIDCDefault { + o._statusCode = code + return o +} + +// SetStatusCode sets the status to the get cluster credentials o ID c default response +func (o *GetClusterCredentialsOIDCDefault) SetStatusCode(code int) { + o._statusCode = code +} + +// WithPayload adds the payload to the get cluster credentials o ID c default response +func (o *GetClusterCredentialsOIDCDefault) WithPayload(payload *models.Error) *GetClusterCredentialsOIDCDefault { + o.Payload = payload + return o +} + +// SetPayload sets the payload to the get cluster credentials o ID c default response +func (o *GetClusterCredentialsOIDCDefault) SetPayload(payload *models.Error) { + o.Payload = payload +} + +// WriteResponse to the client +func (o *GetClusterCredentialsOIDCDefault) WriteResponse(rw http.ResponseWriter, producer runtime.Producer) { + + rw.WriteHeader(o._statusCode) + if o.Payload != nil { + payload := o.Payload + if err := producer.Produce(rw, payload); err != nil { + panic(err) // let the recovery middleware deal with this + } + } +} diff --git a/pkg/api/rest/operations/get_cluster_credentials_o_id_c_urlbuilder.go b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_urlbuilder.go new file mode 100644 index 0000000000..aaf8fb0dac --- /dev/null +++ b/pkg/api/rest/operations/get_cluster_credentials_o_id_c_urlbuilder.go @@ -0,0 +1,96 @@ +// Code generated by go-swagger; DO NOT EDIT. + +package operations + +// This file was generated by the swagger tool. +// Editing this file might prove futile when you re-run the generate command + +import ( + "errors" + "net/url" + golangswaggerpaths "path" + "strings" +) + +// GetClusterCredentialsOIDCURL generates an URL for the get cluster credentials o ID c operation +type GetClusterCredentialsOIDCURL struct { + Name string + + _basePath string + // avoid unkeyed usage + _ struct{} +} + +// WithBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetClusterCredentialsOIDCURL) WithBasePath(bp string) *GetClusterCredentialsOIDCURL { + o.SetBasePath(bp) + return o +} + +// SetBasePath sets the base path for this url builder, only required when it's different from the +// base path specified in the swagger spec. +// When the value of the base path is an empty string +func (o *GetClusterCredentialsOIDCURL) SetBasePath(bp string) { + o._basePath = bp +} + +// Build a url path and query string +func (o *GetClusterCredentialsOIDCURL) Build() (*url.URL, error) { + var _result url.URL + + var _path = "/api/v1/clusters/{name}/credentials/oidc" + + name := o.Name + if name != "" { + _path = strings.Replace(_path, "{name}", name, -1) + } else { + return nil, errors.New("Name is required on GetClusterCredentialsOIDCURL") + } + + _basePath := o._basePath + _result.Path = golangswaggerpaths.Join(_basePath, _path) + + return &_result, nil +} + +// Must is a helper function to panic when the url builder returns an error +func (o *GetClusterCredentialsOIDCURL) Must(u *url.URL, err error) *url.URL { + if err != nil { + panic(err) + } + if u == nil { + panic("url can't be nil") + } + return u +} + +// String returns the string representation of the path with query string +func (o *GetClusterCredentialsOIDCURL) String() string { + return o.Must(o.Build()).String() +} + +// BuildFull builds a full url with scheme, host, path and query string +func (o *GetClusterCredentialsOIDCURL) BuildFull(scheme, host string) (*url.URL, error) { + if scheme == "" { + return nil, errors.New("scheme is required for a full url on GetClusterCredentialsOIDCURL") + } + if host == "" { + return nil, errors.New("host is required for a full url on GetClusterCredentialsOIDCURL") + } + + base, err := o.Build() + if err != nil { + return nil, err + } + + base.Scheme = scheme + base.Host = host + return base, nil +} + +// StringFull returns the string representation of a complete url +func (o *GetClusterCredentialsOIDCURL) StringFull(scheme, host string) string { + return o.Must(o.BuildFull(scheme, host)).String() +} diff --git a/pkg/api/rest/operations/kubernikus_api.go b/pkg/api/rest/operations/kubernikus_api.go index 7ec2688932..0230280eb5 100644 --- a/pkg/api/rest/operations/kubernikus_api.go +++ b/pkg/api/rest/operations/kubernikus_api.go @@ -48,6 +48,9 @@ func NewKubernikusAPI(spec *loads.Document) *KubernikusAPI { GetClusterCredentialsHandler: GetClusterCredentialsHandlerFunc(func(params GetClusterCredentialsParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation GetClusterCredentials has not yet been implemented") }), + GetClusterCredentialsOIDCHandler: GetClusterCredentialsOIDCHandlerFunc(func(params GetClusterCredentialsOIDCParams, principal *models.Principal) middleware.Responder { + return middleware.NotImplemented("operation GetClusterCredentialsOIDC has not yet been implemented") + }), GetClusterEventsHandler: GetClusterEventsHandlerFunc(func(params GetClusterEventsParams, principal *models.Principal) middleware.Responder { return middleware.NotImplemented("operation GetClusterEvents has not yet been implemented") }), @@ -130,6 +133,8 @@ type KubernikusAPI struct { GetBootstrapConfigHandler GetBootstrapConfigHandler // GetClusterCredentialsHandler sets the operation handler for the get cluster credentials operation GetClusterCredentialsHandler GetClusterCredentialsHandler + // GetClusterCredentialsOIDCHandler sets the operation handler for the get cluster credentials o ID c operation + GetClusterCredentialsOIDCHandler GetClusterCredentialsOIDCHandler // GetClusterEventsHandler sets the operation handler for the get cluster events operation GetClusterEventsHandler GetClusterEventsHandler // GetClusterInfoHandler sets the operation handler for the get cluster info operation @@ -229,6 +234,10 @@ func (o *KubernikusAPI) Validate() error { unregistered = append(unregistered, "GetClusterCredentialsHandler") } + if o.GetClusterCredentialsOIDCHandler == nil { + unregistered = append(unregistered, "GetClusterCredentialsOIDCHandler") + } + if o.GetClusterEventsHandler == nil { unregistered = append(unregistered, "GetClusterEventsHandler") } @@ -394,6 +403,11 @@ func (o *KubernikusAPI) initHandlerCache() { } o.handlers["GET"]["/api/v1/clusters/{name}/credentials"] = NewGetClusterCredentials(o.context, o.GetClusterCredentialsHandler) + if o.handlers["GET"] == nil { + o.handlers["GET"] = make(map[string]http.Handler) + } + o.handlers["GET"]["/api/v1/clusters/{name}/credentials/oidc"] = NewGetClusterCredentialsOIDC(o.context, o.GetClusterCredentialsOIDCHandler) + if o.handlers["GET"] == nil { o.handlers["GET"] = make(map[string]http.Handler) } diff --git a/pkg/api/spec/embedded_spec.go b/pkg/api/spec/embedded_spec.go index 181f97cf6a..dcabd31913 100644 --- a/pkg/api/spec/embedded_spec.go +++ b/pkg/api/spec/embedded_spec.go @@ -205,6 +205,32 @@ func init() { } ] }, + "/api/v1/clusters/{name}/credentials/oidc": { + "get": { + "summary": "Get user specific credentials to access the cluster with OIDC", + "operationId": "GetClusterCredentialsOIDC", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "$ref": "#/responses/errorResponse" + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, "/api/v1/clusters/{name}/events": { "get": { "summary": "Get recent events about the cluster", @@ -521,6 +547,9 @@ func init() { "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", "x-nullable": false }, + "dashboard": { + "type": "boolean" + }, "dnsAddress": { "type": "string", "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" @@ -580,6 +609,9 @@ func init() { "chartVersion": { "type": "string" }, + "dashboard": { + "type": "string" + }, "migrationsPending": { "type": "boolean" }, @@ -1139,6 +1171,35 @@ func init() { } ] }, + "/api/v1/clusters/{name}/credentials/oidc": { + "get": { + "summary": "Get user specific credentials to access the cluster with OIDC", + "operationId": "GetClusterCredentialsOIDC", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Credentials" + } + }, + "default": { + "description": "Error", + "schema": { + "$ref": "#/definitions/error" + } + } + } + }, + "parameters": [ + { + "uniqueItems": true, + "type": "string", + "name": "name", + "in": "path", + "required": true + } + ] + }, "/api/v1/clusters/{name}/events": { "get": { "summary": "Get recent events about the cluster", @@ -1438,6 +1499,9 @@ func init() { "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(\\/([0-9]|[1-2][0-9]|3[0-2]))$", "x-nullable": false }, + "dashboard": { + "type": "boolean" + }, "dnsAddress": { "type": "string", "pattern": "^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$" @@ -1497,6 +1561,9 @@ func init() { "chartVersion": { "type": "string" }, + "dashboard": { + "type": "string" + }, "migrationsPending": { "type": "boolean" }, diff --git a/pkg/apis/kubernikus/factory.go b/pkg/apis/kubernikus/factory.go index 8fa8518c06..7c3959bcbd 100644 --- a/pkg/apis/kubernikus/factory.go +++ b/pkg/apis/kubernikus/factory.go @@ -40,6 +40,10 @@ func (klusterFactory) KlusterFor(spec models.KlusterSpec) (*v1.Kluster, error) { spec.NodePools = []models.NodePool{} } + // Enable dex and dashboard for new clusters + spec.Dex = true + spec.Dashboard = true + k := &v1.Kluster{ Spec: spec, Status: models.KlusterStatus{ diff --git a/pkg/apis/kubernikus/v1/secret.go b/pkg/apis/kubernikus/v1/secret.go index d8562091a2..906d5e62c7 100644 --- a/pkg/apis/kubernikus/v1/secret.go +++ b/pkg/apis/kubernikus/v1/secret.go @@ -13,6 +13,9 @@ type Secret struct { NodePassword string `json:"node-password,omitempty"` BootstrapToken string `json:"bootstrapToken"` + DexClientSecret string `json:"dex-client-secret,omitempty"` + DexStaticPassword string `json:"dex-static-password,omitempty"` + Certificates } diff --git a/pkg/client/kubernetes/client.go b/pkg/client/kubernetes/client.go index 6a4177ea72..c06cefdffb 100644 --- a/pkg/client/kubernetes/client.go +++ b/pkg/client/kubernetes/client.go @@ -92,6 +92,48 @@ func NewClientConfigV1(name, user, url string, key, cert, ca []byte, token strin } } +func NewClientConfigV1OIDC(name, user, url, secret, issuer string, ca []byte) clientcmdapiv1.Config { + return clientcmdapiv1.Config{ + APIVersion: "v1", + Kind: "Config", + CurrentContext: name, + Clusters: []clientcmdapiv1.NamedCluster{ + { + Name: name, + Cluster: clientcmdapiv1.Cluster{ + Server: url, + CertificateAuthorityData: ca, + }, + }, + }, + Contexts: []clientcmdapiv1.NamedContext{ + { + Name: name, + Context: clientcmdapiv1.Context{ + Cluster: name, + AuthInfo: user, + }, + }, + }, + AuthInfos: []clientcmdapiv1.NamedAuthInfo{ + { + Name: user, + + AuthInfo: clientcmdapiv1.AuthInfo{ + AuthProvider: &clientcmdapiv1.AuthProviderConfig{ + Name: "oidc", + Config: map[string]string{ + "client-id": "kubernetes", + "client-secret": secret, + "idp-issuer-url": issuer, + }, + }, + }, + }, + }, + } +} + func EnsureCRD(clientset apiextensionsclient.Interface, logger kitlog.Logger) error { klusterCRDName := kubernikus_v1.KlusterResourcePlural + "." + kubernikus_v1.GroupName crd := &apiextensionsv1beta1.CustomResourceDefinition{ diff --git a/pkg/controller/ground.go b/pkg/controller/ground.go index ba19df8592..643dd25def 100644 --- a/pkg/controller/ground.go +++ b/pkg/controller/ground.go @@ -251,6 +251,13 @@ func (op *GroundControl) handler(key string) error { expectedPods = 5 } + if kluster.Spec.Dex { + expectedPods = expectedPods + 1 + if kluster.Spec.Dashboard { + expectedPods = expectedPods + 1 + } + } + op.Logger.Log( "msg", "pod readiness", "kluster", kluster.GetName(), @@ -324,6 +331,7 @@ func (op *GroundControl) handler(key string) error { return err } } + case models.KlusterPhaseUpgrading: updated, err := op.updateVersionStatus(kluster) if err != nil { @@ -510,6 +518,17 @@ func (op *GroundControl) createKluster(kluster *v1.Kluster) error { return fmt.Errorf("Failed to generate node password: %s", err) } + klusterSecret.DexClientSecret, err = goutils.Random(16, 0, 0, true, true, randomPasswordChars...) + if err != nil { + return fmt.Errorf("Failed to generate dex client secret: %s", err) + } + + klusterSecret.DexStaticPassword, err = goutils.Random(16, 0, 0, true, true, randomPasswordChars...) + if err != nil { + return fmt.Errorf("Failed to generate dex static password: %s", err) + + } + certFactory := util.NewCertificateFactory(kluster, &klusterSecret.Certificates, op.Config.Kubernikus.Domain) if _, err := certFactory.Ensure(); err != nil { return fmt.Errorf("Failed to generate certificates: %s", err) @@ -673,7 +692,7 @@ func (op *GroundControl) requiresOpenstackInfo(kluster *v1.Kluster) bool { } func (op *GroundControl) requiresKubernikusInfo(kluster *v1.Kluster) bool { - return kluster.Status.Apiserver == "" || kluster.Status.Wormhole == "" || kluster.Spec.Version == "" + return kluster.Status.Apiserver == "" || kluster.Status.Wormhole == "" || kluster.Spec.Version == "" || (kluster.Spec.Dashboard && kluster.Status.Dashboard == "") } func (op *GroundControl) discoverKubernikusInfo(kluster *v1.Kluster) error { @@ -708,6 +727,15 @@ func (op *GroundControl) discoverKubernikusInfo(kluster *v1.Kluster) error { "project", kluster.Account()) } + if kluster.Spec.Dashboard && kluster.Status.Dashboard == "" { + kluster.Status.Dashboard = fmt.Sprintf("https://dashboard-%s.ingress.%s", kluster.GetName(), op.Config.Kubernikus.Domain) + op.Logger.Log( + "msg", "discovered dashboard URL", + "url", kluster.Status.Wormhole, + "kluster", kluster.GetName(), + "project", kluster.Account()) + } + return nil } diff --git a/pkg/controller/ground/bootstrap.go b/pkg/controller/ground/bootstrap.go index 93eb446d6e..2a7071c8d8 100644 --- a/pkg/controller/ground/bootstrap.go +++ b/pkg/controller/ground/bootstrap.go @@ -70,6 +70,11 @@ func SeedKluster(clients config.Clients, factories config.Factories, kluster *v1 return errors.Wrap(err, "seed GPU support") } } + + if err := SeedOpenStackClusterRoleBindings(kubernetes); err != nil { + return errors.Wrap(err, "seed openstack cluster role bindings") + } + return nil } @@ -310,3 +315,58 @@ func SeedAutoRenewalNodeCertificates(client clientset.Interface) error { }, }) } + +func SeedOpenStackClusterRoleBindings(client clientset.Interface) error { + + err := bootstrap.CreateOrUpdateClusterRoleBinding(client, &rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubernikus:openstack-kubernetes-admin", + }, + RoleRef: rbac.RoleRef{ + APIGroup: rbac.GroupName, + Kind: "ClusterRole", + Name: "cluster-admin", + }, + Subjects: []rbac.Subject{ + { + Kind: "Group", + Name: "openstack_role:kubernetes_admin", + }, + { + Kind: "User", + // It is the marshall & b64enc of the protobuf message IDTokenSubject: https://github.com/dexidp/dex/blob/master/server/oauth2.go#L300 + // User ID: 00000000-0000-0000-0000-000000000001 ConnID: local + Name: "CiQwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDESBWxvY2Fs", + // For claims, we are using "sub" instead of "email" since some technical users missing emails + // If we switch to email, we can directly use email as Name field above + }, + }, + }) + + if err != nil { + return err + } + + err = bootstrap.CreateOrUpdateClusterRoleBinding(client, &rbac.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "kubernikus:openstack-kubernetes-member", + }, + RoleRef: rbac.RoleRef{ + APIGroup: rbac.GroupName, + Kind: "ClusterRole", + Name: "view", + }, + Subjects: []rbac.Subject{ + { + Kind: "Group", + Name: "openstack_role:kubernetes_member", + }, + }, + }) + + if err != nil { + return err + } + + return nil +} diff --git a/pkg/migration/17_dex.go b/pkg/migration/17_dex.go new file mode 100644 index 0000000000..445e586c06 --- /dev/null +++ b/pkg/migration/17_dex.go @@ -0,0 +1,54 @@ +package migration + +import ( + "fmt" + + "github.com/aokoli/goutils" + + v1 "github.com/sapcc/kubernikus/pkg/apis/kubernikus/v1" + "github.com/sapcc/kubernikus/pkg/controller/config" + "github.com/sapcc/kubernikus/pkg/controller/ground" + "github.com/sapcc/kubernikus/pkg/util" +) + +func AddDexSecretAndRoleBindings(rawKluster []byte, current *v1.Kluster, clients config.Clients, factories config.Factories) (err error) { + + // Secret + apiSecret, err := util.KlusterSecret(clients.Kubernetes, current) + if err != nil { + return fmt.Errorf("Failed to serialize secret data: %s", err) + } + + var randomPasswordChars = []rune("abcdefghjkmnpqrstuvwxABCDEFGHJKLMNPQRSTUVWX23456789") + + if apiSecret.DexClientSecret == "" { + apiSecret.DexClientSecret, err = goutils.Random(16, 0, 0, true, true, randomPasswordChars...) + if err != nil { + return fmt.Errorf("Failed to generate dex client secret: %s", err) + } + } + + if apiSecret.DexStaticPassword == "" { + apiSecret.DexStaticPassword, err = goutils.Random(16, 0, 0, true, true, randomPasswordChars...) + if err != nil { + return fmt.Errorf("Failed to generate dex static password: %s", err) + } + } + + err = util.UpdateKlusterSecret(clients.Kubernetes, current, apiSecret) + if err != nil { + return err + } + + // Seed dex cluster role bindings + kubernetes, err := clients.Satellites.ClientFor(current) + if err != nil { + return err + } + err = ground.SeedOpenStackClusterRoleBindings(kubernetes) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/migration/register.go b/pkg/migration/register.go index 2938772077..969a77a033 100644 --- a/pkg/migration/register.go +++ b/pkg/migration/register.go @@ -23,6 +23,7 @@ func init() { CleanupSuppositoryNamespaces, ReconcileNodePoolConfigDefaults, FixUpdateConf, + AddDexSecretAndRoleBindings, // <-- Insert new migrations at the end only! } } diff --git a/pkg/util/helm/helm.go b/pkg/util/helm/helm.go index a301a481b5..3cd93df273 100644 --- a/pkg/util/helm/helm.go +++ b/pkg/util/helm/helm.go @@ -6,6 +6,8 @@ import ( "math/rand" "net/url" + "golang.org/x/crypto/bcrypt" + yaml "gopkg.in/yaml.v2" v1 "github.com/sapcc/kubernikus/pkg/apis/kubernikus/v1" @@ -67,6 +69,20 @@ type versionValues struct { Kubernikus string `yaml:"kubernikus,omitempty"` } +type dashboardValues struct { + Enabled bool `yaml:"enabled,omitempty"` +} + +type dexValues struct { + Enabled bool `yaml:"enabled,omitempty"` + StaticClientSecret string `yaml:"staticClientSecret,omitempty"` + StaticPassword staticPassword `yaml:"staticPasword,omitempty"` +} + +type staticPassword struct { + HashedPassword string `yaml:"hashedPassword,omitempty"` +} + type kubernikusHelmValues struct { Openstack openstackValues `yaml:"openstack,omitempty"` ClusterCIDR string `yaml:"clusterCIDR,omitempty"` @@ -80,6 +96,8 @@ type kubernikusHelmValues struct { Account string `yaml:"account"` SecretName string `yaml:"secretName"` ImageRegistry version.ImageRegistry `yaml:",inline"` + Dex dexValues `yaml:"dex,omitempty"` + Dashboard dashboardValues `yaml:"dashboard,omitempty"` } func KlusterToHelmValues(kluster *v1.Kluster, secret *v1.Secret, kubernetesVersion string, registry *version.ImageRegistry, accessMode string) ([]byte, error) { @@ -98,6 +116,16 @@ func KlusterToHelmValues(kluster *v1.Kluster, secret *v1.Secret, kubernetesVersi uidChecksum := crc64.Checksum([]byte(kluster.UID), crc64ISOTable) backupMinute := rand.New(rand.NewSource(int64(uidChecksum))).Intn(60) + hashedPassword := "" + if kluster.Spec.Dex { + + hashedBytes, err := bcrypt.GenerateFromPassword([]byte(secret.DexStaticPassword), bcrypt.DefaultCost) + if err != nil { + return nil, fmt.Errorf("Failed to hash dex static password: %v", err) + } + hashedPassword = string(hashedBytes) + } + values := kubernikusHelmValues{ Account: kluster.Account(), BoostrapToken: secret.BootstrapToken, @@ -138,6 +166,16 @@ func KlusterToHelmValues(kluster *v1.Kluster, secret *v1.Secret, kubernetesVersi ApiserverHost: apiserverURL.Hostname(), WormholeHost: wormholeURL.Hostname(), }, + Dashboard: dashboardValues{ + Enabled: kluster.Spec.Dashboard, + }, + Dex: dexValues{ + Enabled: kluster.Spec.Dex, + StaticPassword: staticPassword{ + HashedPassword: hashedPassword, + }, + StaticClientSecret: secret.DexClientSecret, + }, } if !kluster.Spec.NoCloud { values.Openstack = openstackValues{ diff --git a/pkg/version/images.go b/pkg/version/images.go index f6908de9e4..e91e4752f5 100644 --- a/pkg/version/images.go +++ b/pkg/version/images.go @@ -25,6 +25,9 @@ type KlusterVersion struct { Supported bool `yaml:"supported"` Hyperkube ImageVersion `yaml:"hyperkube"` CloudControllerManager ImageVersion `yaml:"cloudControllerManager"` + Dex ImageVersion `yaml:"dex,omitempty"` + Dashboard ImageVersion `yaml:"dashboard,omitempty"` + DashboardProxy ImageVersion `yaml:"dashboardProxy,omitempty"` } type ImageRegistry struct { diff --git a/swagger.yml b/swagger.yml index fed5df77fe..f4ae750d7e 100644 --- a/swagger.yml +++ b/swagger.yml @@ -138,6 +138,23 @@ paths: $ref: '#/definitions/Credentials' default: $ref: '#/responses/errorResponse' + '/api/v1/clusters/{name}/credentials/oidc': + parameters: + - uniqueItems: true + type: string + name: name + required: true + in: path + get: + operationId: GetClusterCredentialsOIDC + summary: Get user specific credentials to access the cluster with OIDC + responses: + '200': + description: OK + schema: + $ref: '#/definitions/Credentials' + default: + $ref: '#/responses/errorResponse' '/api/v1/clusters/{name}/bootstrap': parameters: - uniqueItems: true @@ -403,6 +420,10 @@ definitions: $ref: '#/definitions/OpenstackSpec' noCloud: type: boolean + dashboard: + type: boolean + dex: + type: boolean serviceCIDR: description: CIDR Range for Services in the cluster. Can not be updated. default: 198.18.128.0/17 @@ -538,6 +559,8 @@ definitions: $ref: '#/definitions/NodePoolInfo' apiserver: type: string + dashboard: + type: string apiserverVersion: type: string chartName: @@ -626,5 +649,4 @@ definitions: helpUrl: description: link to help page explaining the error in more detail type: string - format: uri - + format: uri \ No newline at end of file diff --git a/terraform/kubernikus.tf b/terraform/kubernikus.tf index 272a7a3efb..412e0a32ff 100644 --- a/terraform/kubernikus.tf +++ b/terraform/kubernikus.tf @@ -487,6 +487,15 @@ resource "openstack_dns_recordset_v2" "wildcard-kubernikus" { records = ["kubernikus-k8sniff.${var.region}.cloud.sap."] } +resource "openstack_dns_recordset_v2" "wildcard-kubernikus-ingress" { + provider = "openstack.master" + zone_id = "${data.openstack_dns_zone_v2.region_cloud_sap.id}" + name = "*.ingress.kubernikus.${var.region}.cloud.sap." + type = "CNAME" + ttl = 1800 + records = ["kubernikus-ingress.${var.region}.cloud.sap."] +} + resource "openstack_dns_recordset_v2" "kubernikus" { provider = "openstack.master" zone_id = "${data.openstack_dns_zone_v2.region_cloud_sap.id}" diff --git a/vendor/golang.org/x/crypto/bcrypt/base64.go b/vendor/golang.org/x/crypto/bcrypt/base64.go new file mode 100644 index 0000000000..fc31160908 --- /dev/null +++ b/vendor/golang.org/x/crypto/bcrypt/base64.go @@ -0,0 +1,35 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package bcrypt + +import "encoding/base64" + +const alphabet = "./ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" + +var bcEncoding = base64.NewEncoding(alphabet) + +func base64Encode(src []byte) []byte { + n := bcEncoding.EncodedLen(len(src)) + dst := make([]byte, n) + bcEncoding.Encode(dst, src) + for dst[n-1] == '=' { + n-- + } + return dst[:n] +} + +func base64Decode(src []byte) ([]byte, error) { + numOfEquals := 4 - (len(src) % 4) + for i := 0; i < numOfEquals; i++ { + src = append(src, '=') + } + + dst := make([]byte, bcEncoding.DecodedLen(len(src))) + n, err := bcEncoding.Decode(dst, src) + if err != nil { + return nil, err + } + return dst[:n], nil +} diff --git a/vendor/golang.org/x/crypto/bcrypt/bcrypt.go b/vendor/golang.org/x/crypto/bcrypt/bcrypt.go new file mode 100644 index 0000000000..202fa8aff4 --- /dev/null +++ b/vendor/golang.org/x/crypto/bcrypt/bcrypt.go @@ -0,0 +1,295 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package bcrypt implements Provos and Mazières's bcrypt adaptive hashing +// algorithm. See http://www.usenix.org/event/usenix99/provos/provos.pdf +package bcrypt // import "golang.org/x/crypto/bcrypt" + +// The code is a port of Provos and Mazières's C implementation. +import ( + "crypto/rand" + "crypto/subtle" + "errors" + "fmt" + "io" + "strconv" + + "golang.org/x/crypto/blowfish" +) + +const ( + MinCost int = 4 // the minimum allowable cost as passed in to GenerateFromPassword + MaxCost int = 31 // the maximum allowable cost as passed in to GenerateFromPassword + DefaultCost int = 10 // the cost that will actually be set if a cost below MinCost is passed into GenerateFromPassword +) + +// The error returned from CompareHashAndPassword when a password and hash do +// not match. +var ErrMismatchedHashAndPassword = errors.New("crypto/bcrypt: hashedPassword is not the hash of the given password") + +// The error returned from CompareHashAndPassword when a hash is too short to +// be a bcrypt hash. +var ErrHashTooShort = errors.New("crypto/bcrypt: hashedSecret too short to be a bcrypted password") + +// The error returned from CompareHashAndPassword when a hash was created with +// a bcrypt algorithm newer than this implementation. +type HashVersionTooNewError byte + +func (hv HashVersionTooNewError) Error() string { + return fmt.Sprintf("crypto/bcrypt: bcrypt algorithm version '%c' requested is newer than current version '%c'", byte(hv), majorVersion) +} + +// The error returned from CompareHashAndPassword when a hash starts with something other than '$' +type InvalidHashPrefixError byte + +func (ih InvalidHashPrefixError) Error() string { + return fmt.Sprintf("crypto/bcrypt: bcrypt hashes must start with '$', but hashedSecret started with '%c'", byte(ih)) +} + +type InvalidCostError int + +func (ic InvalidCostError) Error() string { + return fmt.Sprintf("crypto/bcrypt: cost %d is outside allowed range (%d,%d)", int(ic), int(MinCost), int(MaxCost)) +} + +const ( + majorVersion = '2' + minorVersion = 'a' + maxSaltSize = 16 + maxCryptedHashSize = 23 + encodedSaltSize = 22 + encodedHashSize = 31 + minHashSize = 59 +) + +// magicCipherData is an IV for the 64 Blowfish encryption calls in +// bcrypt(). It's the string "OrpheanBeholderScryDoubt" in big-endian bytes. +var magicCipherData = []byte{ + 0x4f, 0x72, 0x70, 0x68, + 0x65, 0x61, 0x6e, 0x42, + 0x65, 0x68, 0x6f, 0x6c, + 0x64, 0x65, 0x72, 0x53, + 0x63, 0x72, 0x79, 0x44, + 0x6f, 0x75, 0x62, 0x74, +} + +type hashed struct { + hash []byte + salt []byte + cost int // allowed range is MinCost to MaxCost + major byte + minor byte +} + +// GenerateFromPassword returns the bcrypt hash of the password at the given +// cost. If the cost given is less than MinCost, the cost will be set to +// DefaultCost, instead. Use CompareHashAndPassword, as defined in this package, +// to compare the returned hashed password with its cleartext version. +func GenerateFromPassword(password []byte, cost int) ([]byte, error) { + p, err := newFromPassword(password, cost) + if err != nil { + return nil, err + } + return p.Hash(), nil +} + +// CompareHashAndPassword compares a bcrypt hashed password with its possible +// plaintext equivalent. Returns nil on success, or an error on failure. +func CompareHashAndPassword(hashedPassword, password []byte) error { + p, err := newFromHash(hashedPassword) + if err != nil { + return err + } + + otherHash, err := bcrypt(password, p.cost, p.salt) + if err != nil { + return err + } + + otherP := &hashed{otherHash, p.salt, p.cost, p.major, p.minor} + if subtle.ConstantTimeCompare(p.Hash(), otherP.Hash()) == 1 { + return nil + } + + return ErrMismatchedHashAndPassword +} + +// Cost returns the hashing cost used to create the given hashed +// password. When, in the future, the hashing cost of a password system needs +// to be increased in order to adjust for greater computational power, this +// function allows one to establish which passwords need to be updated. +func Cost(hashedPassword []byte) (int, error) { + p, err := newFromHash(hashedPassword) + if err != nil { + return 0, err + } + return p.cost, nil +} + +func newFromPassword(password []byte, cost int) (*hashed, error) { + if cost < MinCost { + cost = DefaultCost + } + p := new(hashed) + p.major = majorVersion + p.minor = minorVersion + + err := checkCost(cost) + if err != nil { + return nil, err + } + p.cost = cost + + unencodedSalt := make([]byte, maxSaltSize) + _, err = io.ReadFull(rand.Reader, unencodedSalt) + if err != nil { + return nil, err + } + + p.salt = base64Encode(unencodedSalt) + hash, err := bcrypt(password, p.cost, p.salt) + if err != nil { + return nil, err + } + p.hash = hash + return p, err +} + +func newFromHash(hashedSecret []byte) (*hashed, error) { + if len(hashedSecret) < minHashSize { + return nil, ErrHashTooShort + } + p := new(hashed) + n, err := p.decodeVersion(hashedSecret) + if err != nil { + return nil, err + } + hashedSecret = hashedSecret[n:] + n, err = p.decodeCost(hashedSecret) + if err != nil { + return nil, err + } + hashedSecret = hashedSecret[n:] + + // The "+2" is here because we'll have to append at most 2 '=' to the salt + // when base64 decoding it in expensiveBlowfishSetup(). + p.salt = make([]byte, encodedSaltSize, encodedSaltSize+2) + copy(p.salt, hashedSecret[:encodedSaltSize]) + + hashedSecret = hashedSecret[encodedSaltSize:] + p.hash = make([]byte, len(hashedSecret)) + copy(p.hash, hashedSecret) + + return p, nil +} + +func bcrypt(password []byte, cost int, salt []byte) ([]byte, error) { + cipherData := make([]byte, len(magicCipherData)) + copy(cipherData, magicCipherData) + + c, err := expensiveBlowfishSetup(password, uint32(cost), salt) + if err != nil { + return nil, err + } + + for i := 0; i < 24; i += 8 { + for j := 0; j < 64; j++ { + c.Encrypt(cipherData[i:i+8], cipherData[i:i+8]) + } + } + + // Bug compatibility with C bcrypt implementations. We only encode 23 of + // the 24 bytes encrypted. + hsh := base64Encode(cipherData[:maxCryptedHashSize]) + return hsh, nil +} + +func expensiveBlowfishSetup(key []byte, cost uint32, salt []byte) (*blowfish.Cipher, error) { + csalt, err := base64Decode(salt) + if err != nil { + return nil, err + } + + // Bug compatibility with C bcrypt implementations. They use the trailing + // NULL in the key string during expansion. + // We copy the key to prevent changing the underlying array. + ckey := append(key[:len(key):len(key)], 0) + + c, err := blowfish.NewSaltedCipher(ckey, csalt) + if err != nil { + return nil, err + } + + var i, rounds uint64 + rounds = 1 << cost + for i = 0; i < rounds; i++ { + blowfish.ExpandKey(ckey, c) + blowfish.ExpandKey(csalt, c) + } + + return c, nil +} + +func (p *hashed) Hash() []byte { + arr := make([]byte, 60) + arr[0] = '$' + arr[1] = p.major + n := 2 + if p.minor != 0 { + arr[2] = p.minor + n = 3 + } + arr[n] = '$' + n += 1 + copy(arr[n:], []byte(fmt.Sprintf("%02d", p.cost))) + n += 2 + arr[n] = '$' + n += 1 + copy(arr[n:], p.salt) + n += encodedSaltSize + copy(arr[n:], p.hash) + n += encodedHashSize + return arr[:n] +} + +func (p *hashed) decodeVersion(sbytes []byte) (int, error) { + if sbytes[0] != '$' { + return -1, InvalidHashPrefixError(sbytes[0]) + } + if sbytes[1] > majorVersion { + return -1, HashVersionTooNewError(sbytes[1]) + } + p.major = sbytes[1] + n := 3 + if sbytes[2] != '$' { + p.minor = sbytes[2] + n++ + } + return n, nil +} + +// sbytes should begin where decodeVersion left off. +func (p *hashed) decodeCost(sbytes []byte) (int, error) { + cost, err := strconv.Atoi(string(sbytes[0:2])) + if err != nil { + return -1, err + } + err = checkCost(cost) + if err != nil { + return -1, err + } + p.cost = cost + return 3, nil +} + +func (p *hashed) String() string { + return fmt.Sprintf("&{hash: %#v, salt: %#v, cost: %d, major: %c, minor: %c}", string(p.hash), p.salt, p.cost, p.major, p.minor) +} + +func checkCost(cost int) error { + if cost < MinCost || cost > MaxCost { + return InvalidCostError(cost) + } + return nil +} diff --git a/vendor/golang.org/x/crypto/blowfish/block.go b/vendor/golang.org/x/crypto/blowfish/block.go new file mode 100644 index 0000000000..9d80f19521 --- /dev/null +++ b/vendor/golang.org/x/crypto/blowfish/block.go @@ -0,0 +1,159 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package blowfish + +// getNextWord returns the next big-endian uint32 value from the byte slice +// at the given position in a circular manner, updating the position. +func getNextWord(b []byte, pos *int) uint32 { + var w uint32 + j := *pos + for i := 0; i < 4; i++ { + w = w<<8 | uint32(b[j]) + j++ + if j >= len(b) { + j = 0 + } + } + *pos = j + return w +} + +// ExpandKey performs a key expansion on the given *Cipher. Specifically, it +// performs the Blowfish algorithm's key schedule which sets up the *Cipher's +// pi and substitution tables for calls to Encrypt. This is used, primarily, +// by the bcrypt package to reuse the Blowfish key schedule during its +// set up. It's unlikely that you need to use this directly. +func ExpandKey(key []byte, c *Cipher) { + j := 0 + for i := 0; i < 18; i++ { + // Using inlined getNextWord for performance. + var d uint32 + for k := 0; k < 4; k++ { + d = d<<8 | uint32(key[j]) + j++ + if j >= len(key) { + j = 0 + } + } + c.p[i] ^= d + } + + var l, r uint32 + for i := 0; i < 18; i += 2 { + l, r = encryptBlock(l, r, c) + c.p[i], c.p[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s0[i], c.s0[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s1[i], c.s1[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s2[i], c.s2[i+1] = l, r + } + for i := 0; i < 256; i += 2 { + l, r = encryptBlock(l, r, c) + c.s3[i], c.s3[i+1] = l, r + } +} + +// This is similar to ExpandKey, but folds the salt during the key +// schedule. While ExpandKey is essentially expandKeyWithSalt with an all-zero +// salt passed in, reusing ExpandKey turns out to be a place of inefficiency +// and specializing it here is useful. +func expandKeyWithSalt(key []byte, salt []byte, c *Cipher) { + j := 0 + for i := 0; i < 18; i++ { + c.p[i] ^= getNextWord(key, &j) + } + + j = 0 + var l, r uint32 + for i := 0; i < 18; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.p[i], c.p[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s0[i], c.s0[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s1[i], c.s1[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s2[i], c.s2[i+1] = l, r + } + + for i := 0; i < 256; i += 2 { + l ^= getNextWord(salt, &j) + r ^= getNextWord(salt, &j) + l, r = encryptBlock(l, r, c) + c.s3[i], c.s3[i+1] = l, r + } +} + +func encryptBlock(l, r uint32, c *Cipher) (uint32, uint32) { + xl, xr := l, r + xl ^= c.p[0] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[1] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[2] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[3] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[4] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[5] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[6] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[7] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[8] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[9] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[10] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[11] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[12] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[13] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[14] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[15] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[16] + xr ^= c.p[17] + return xr, xl +} + +func decryptBlock(l, r uint32, c *Cipher) (uint32, uint32) { + xl, xr := l, r + xl ^= c.p[17] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[16] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[15] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[14] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[13] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[12] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[11] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[10] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[9] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[8] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[7] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[6] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[5] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[4] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[3] + xr ^= ((c.s0[byte(xl>>24)] + c.s1[byte(xl>>16)]) ^ c.s2[byte(xl>>8)]) + c.s3[byte(xl)] ^ c.p[2] + xl ^= ((c.s0[byte(xr>>24)] + c.s1[byte(xr>>16)]) ^ c.s2[byte(xr>>8)]) + c.s3[byte(xr)] ^ c.p[1] + xr ^= c.p[0] + return xr, xl +} diff --git a/vendor/golang.org/x/crypto/blowfish/cipher.go b/vendor/golang.org/x/crypto/blowfish/cipher.go new file mode 100644 index 0000000000..2641dadd64 --- /dev/null +++ b/vendor/golang.org/x/crypto/blowfish/cipher.go @@ -0,0 +1,91 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package blowfish implements Bruce Schneier's Blowfish encryption algorithm. +package blowfish // import "golang.org/x/crypto/blowfish" + +// The code is a port of Bruce Schneier's C implementation. +// See https://www.schneier.com/blowfish.html. + +import "strconv" + +// The Blowfish block size in bytes. +const BlockSize = 8 + +// A Cipher is an instance of Blowfish encryption using a particular key. +type Cipher struct { + p [18]uint32 + s0, s1, s2, s3 [256]uint32 +} + +type KeySizeError int + +func (k KeySizeError) Error() string { + return "crypto/blowfish: invalid key size " + strconv.Itoa(int(k)) +} + +// NewCipher creates and returns a Cipher. +// The key argument should be the Blowfish key, from 1 to 56 bytes. +func NewCipher(key []byte) (*Cipher, error) { + var result Cipher + if k := len(key); k < 1 || k > 56 { + return nil, KeySizeError(k) + } + initCipher(&result) + ExpandKey(key, &result) + return &result, nil +} + +// NewSaltedCipher creates a returns a Cipher that folds a salt into its key +// schedule. For most purposes, NewCipher, instead of NewSaltedCipher, is +// sufficient and desirable. For bcrypt compatibility, the key can be over 56 +// bytes. +func NewSaltedCipher(key, salt []byte) (*Cipher, error) { + if len(salt) == 0 { + return NewCipher(key) + } + var result Cipher + if k := len(key); k < 1 { + return nil, KeySizeError(k) + } + initCipher(&result) + expandKeyWithSalt(key, salt, &result) + return &result, nil +} + +// BlockSize returns the Blowfish block size, 8 bytes. +// It is necessary to satisfy the Block interface in the +// package "crypto/cipher". +func (c *Cipher) BlockSize() int { return BlockSize } + +// Encrypt encrypts the 8-byte buffer src using the key k +// and stores the result in dst. +// Note that for amounts of data larger than a block, +// it is not safe to just call Encrypt on successive blocks; +// instead, use an encryption mode like CBC (see crypto/cipher/cbc.go). +func (c *Cipher) Encrypt(dst, src []byte) { + l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3]) + r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7]) + l, r = encryptBlock(l, r, c) + dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l) + dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r) +} + +// Decrypt decrypts the 8-byte buffer src using the key k +// and stores the result in dst. +func (c *Cipher) Decrypt(dst, src []byte) { + l := uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3]) + r := uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7]) + l, r = decryptBlock(l, r, c) + dst[0], dst[1], dst[2], dst[3] = byte(l>>24), byte(l>>16), byte(l>>8), byte(l) + dst[4], dst[5], dst[6], dst[7] = byte(r>>24), byte(r>>16), byte(r>>8), byte(r) +} + +func initCipher(c *Cipher) { + copy(c.p[0:], p[0:]) + copy(c.s0[0:], s0[0:]) + copy(c.s1[0:], s1[0:]) + copy(c.s2[0:], s2[0:]) + copy(c.s3[0:], s3[0:]) +} diff --git a/vendor/golang.org/x/crypto/blowfish/const.go b/vendor/golang.org/x/crypto/blowfish/const.go new file mode 100644 index 0000000000..d04077595a --- /dev/null +++ b/vendor/golang.org/x/crypto/blowfish/const.go @@ -0,0 +1,199 @@ +// Copyright 2010 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// The startup permutation array and substitution boxes. +// They are the hexadecimal digits of PI; see: +// https://www.schneier.com/code/constants.txt. + +package blowfish + +var s0 = [256]uint32{ + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, 0xb8e1afed, 0x6a267e96, + 0xba7c9045, 0xf12c7f99, 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, 0x0d95748f, 0x728eb658, + 0x718bcd58, 0x82154aee, 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, 0x8e79dcb0, 0x603a180e, + 0x6c9e0e8b, 0xb01e8a3e, 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, 0x55ca396a, 0x2aab10b6, + 0xb4cc5c34, 0x1141e8ce, 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, 0xafd6ba33, 0x6c24cf5c, + 0x7a325381, 0x28958677, 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, 0xef845d5d, 0xe98575b1, + 0xdc262302, 0xeb651b88, 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, 0x21c66842, 0xf6e96c9a, + 0x670c9c61, 0xabd388f0, 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, 0xa1f1651d, 0x39af0176, + 0x66ca593e, 0x82430e88, 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, 0x4ed3aa62, 0x363f7706, + 0x1bfedf72, 0x429b023d, 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, 0xe3fe501a, 0xb6794c3b, + 0x976ce0bd, 0x04c006ba, 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, 0x6dfc511f, 0x9b30952c, + 0xcc814544, 0xaf5ebd09, 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, 0x5579c0bd, 0x1a60320a, + 0xd6a100c6, 0x402c7279, 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, 0x323db5fa, 0xfd238760, + 0x53317b48, 0x3e00df82, 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, 0x695b27b0, 0xbbca58c8, + 0xe1ffa35d, 0xb8f011a0, 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, 0xe1ddf2da, 0xa4cb7e33, + 0x62fb1341, 0xcee4c6e8, 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, 0xd08ed1d0, 0xafc725e0, + 0x8e3c5b2f, 0x8e7594b7, 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, 0x2f2f2218, 0xbe0e1777, + 0xea752dfe, 0x8b021fa1, 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, 0x165fa266, 0x80957705, + 0x93cc7314, 0x211a1477, 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, 0x00250e2d, 0x2071b35e, + 0x226800bb, 0x57b8e0af, 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, 0x83260376, 0x6295cfa9, + 0x11c81968, 0x4e734a41, 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, 0x08ba6fb5, 0x571be91f, + 0xf296ec6b, 0x2a0dd915, 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, +} + +var s1 = [256]uint32{ + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, 0xad6ea6b0, 0x49a7df7d, + 0x9cee60b8, 0x8fedb266, 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, 0x3f54989a, 0x5b429d65, + 0x6b8fe4d6, 0x99f73fd6, 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, 0x09686b3f, 0x3ebaefc9, + 0x3c971814, 0x6b6a70a1, 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, 0xb03ada37, 0xf0500c0d, + 0xf01c1f04, 0x0200b3ff, 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, 0x3ae5e581, 0x37c2dadc, + 0xc8b57634, 0x9af3dda7, 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, 0x4e548b38, 0x4f6db908, + 0x6f420d03, 0xf60a04bf, 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, 0x5512721f, 0x2e6b7124, + 0x501adde6, 0x9f84cd87, 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, 0xef1c1847, 0x3215d908, + 0xdd433b37, 0x24c2ba16, 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, 0x043556f1, 0xd7a3c76b, + 0x3c11183b, 0x5924a509, 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, 0x771fe71c, 0x4e3d06fa, + 0x2965dcb9, 0x99e71d0f, 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, 0xf2f74ea7, 0x361d2b3d, + 0x1939260f, 0x19c27960, 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, 0xc332ddef, 0xbe6c5aa5, + 0x65582185, 0x68ab9802, 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, 0x13cca830, 0xeb61bd96, + 0x0334fe1e, 0xaa0363cf, 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, 0x648b1eaf, 0x19bdf0ca, + 0xa02369b9, 0x655abb50, 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, 0xf837889a, 0x97e32d77, + 0x11ed935f, 0x16681281, 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, 0xcdb30aeb, 0x532e3054, + 0x8fd948e4, 0x6dbc3128, 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, 0x45eee2b6, 0xa3aaabea, + 0xdb6c4f15, 0xfacb4fd0, 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, 0xcf62a1f2, 0x5b8d2646, + 0xfc8883a0, 0xc1c7b6a3, 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, 0x58428d2a, 0x0c55f5ea, + 0x1dadf43e, 0x233f7061, 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, 0xa6078084, 0x19f8509e, + 0xe8efd855, 0x61d99735, 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, 0xdb73dbd3, 0x105588cd, + 0x675fda79, 0xe3674340, 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, +} + +var s2 = [256]uint32{ + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, 0x411520f7, 0x7602d4f7, + 0xbcf46b2e, 0xd4a20068, 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, 0x4d95fc1d, 0x96b591af, + 0x70f4ddd3, 0x66a02f45, 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, 0x28507825, 0x530429f4, + 0x0a2c86da, 0xe9b66dfb, 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, 0xaace1e7c, 0xd3375fec, + 0xce78a399, 0x406b2a42, 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, 0x3a6efa74, 0xdd5b4332, + 0x6841e7f7, 0xca7820fb, 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, 0x55a867bc, 0xa1159a58, + 0xcca92963, 0x99e1db33, 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, 0x95c11548, 0xe4c66d22, + 0x48c1133f, 0xc70f86dc, 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, 0x257b7834, 0x602a9c60, + 0xdff8e8a3, 0x1f636c1b, 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, 0x85b2a20e, 0xe6ba0d99, + 0xde720c8c, 0x2da2f728, 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, 0x0a476341, 0x992eff74, + 0x3a6f6eab, 0xf4f8fd37, 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, 0xf1290dc7, 0xcc00ffa3, + 0xb5390f92, 0x690fed0b, 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, 0x37392eb3, 0xcc115979, + 0x8026e297, 0xf42e312d, 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, 0x1a6b1018, 0x11caedfa, + 0x3d25bdd8, 0xe2e1c3c9, 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, 0x9dbc8057, 0xf0f7c086, + 0x60787bf8, 0x6003604d, 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, 0x77a057be, 0xbde8ae24, + 0x55464299, 0xbf582e61, 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, 0x7aeb2661, 0x8b1ddf84, + 0x846a0e79, 0x915f95e2, 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, 0xb77f19b6, 0xe0a9dc09, + 0x662d09a1, 0xc4324633, 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, 0xdcb7da83, 0x573906fe, + 0xa1e2ce9b, 0x4fcd7f52, 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, 0xf0177a28, 0xc0f586e0, + 0x006058aa, 0x30dc7d62, 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, 0x6f05e409, 0x4b7c0188, + 0x39720a3d, 0x7c927c24, 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, 0x1e50ef5e, 0xb161e6f8, + 0xa28514d9, 0x6c51133c, 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, +} + +var s3 = [256]uint32{ + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, 0x5cb0679e, 0x4fa33742, + 0xd3822740, 0x99bc9bbe, 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, 0x5748ab2f, 0xbc946e79, + 0xc6a376d2, 0x6549c2c8, 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, 0xa1fad5f0, 0x6a2d519a, + 0x63ef8ce2, 0x9a86ee22, 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, 0x2826a2f9, 0xa73a3ae1, + 0x4ba99586, 0xef5562e9, 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, 0xe990fd5a, 0x9e34d797, + 0x2cf0b7d9, 0x022b8b51, 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, 0xe029ac71, 0xe019a5e6, + 0x47b0acfd, 0xed93fa9b, 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, 0x15056dd4, 0x88f46dba, + 0x03a16125, 0x0564f0bd, 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, 0x7533d928, 0xb155fdf5, + 0x03563482, 0x8aba3cbb, 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, 0xea7a90c2, 0xfb3e7bce, + 0x5121ce64, 0x774fbe32, 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, 0xb39a460a, 0x6445c0dd, + 0x586cdecf, 0x1c20c8ae, 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, 0x72eacea8, 0xfa6484bb, + 0x8d6612ae, 0xbf3c6f47, 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, 0x4040cb08, 0x4eb4e2cc, + 0x34d2466a, 0x0115af84, 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, 0x611560b1, 0xe7933fdc, + 0xbb3a792b, 0x344525bd, 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, 0x1a908749, 0xd44fbd9a, + 0xd0dadecb, 0xd50ada38, 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, 0xbf97222c, 0x15e6fc2a, + 0x0f91fc71, 0x9b941525, 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, 0xe0ec6e0e, 0x1698db3b, + 0x4c98a0be, 0x3278e964, 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, 0xdf359f8d, 0x9b992f2e, + 0xe60b6f47, 0x0fe3f11d, 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, 0xf523f357, 0xa6327623, + 0x93a83531, 0x56cccd02, 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, 0xe6c6c7bd, 0x327a140a, + 0x45e1d006, 0xc3f27b9a, 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, 0x53113ec0, 0x1640e3d3, + 0x38abbd60, 0x2547adf0, 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, 0x1948c25c, 0x02fb8a8c, + 0x01c36ae4, 0xd6ebe1f9, 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6, +} + +var p = [18]uint32{ + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, 0xa4093822, 0x299f31d0, + 0x082efa98, 0xec4e6c89, 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, 0x9216d5d9, 0x8979fb1b, +}