diff --git a/Makefile b/Makefile index 6dde3bb..071c40e 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ REVISION := $(shell git rev-parse HEAD) # for your host's architecture. The latter are going to run in Kubernetes, so # want to be amd64. CONTROLLERS = \ - unikorn-region-controller + unikorn-region-controller \ + unikorn-identity-controller # Release will do cross compliation of all images for the 'all' target. # Note we aren't fucking about with docker here because that opens up a diff --git a/charts/region/Chart.yaml b/charts/region/Chart.yaml index 4afe2fe..ae5053f 100644 --- a/charts/region/Chart.yaml +++ b/charts/region/Chart.yaml @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn's Region Controller type: application -version: v0.1.32 -appVersion: v0.1.32 +version: v0.1.33 +appVersion: v0.1.33 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png diff --git a/charts/region/crds/region.unikorn-cloud.org_identities.yaml b/charts/region/crds/region.unikorn-cloud.org_identities.yaml index 503b235..1fcae08 100644 --- a/charts/region/crds/region.unikorn-cloud.org_identities.yaml +++ b/charts/region/crds/region.unikorn-cloud.org_identities.yaml @@ -57,38 +57,9 @@ spec: spec: description: IdentitySpec stores any state necessary to manage identity. properties: - openstack: - description: OpenStack is populated when the provider type is set - to "openstack". - properties: - cloud: - description: Cloud is the cloud name in the cloud config to use. - type: string - cloudConfig: - description: CloudConfig is a client compatible cloud configuration. - format: byte - type: string - password: - description: Password is the login for the user. - type: string - projectID: - description: ProjectID is the ID of the project created for the - identity. - type: string - serverGroupID: - description: ServerGroupID is the ID of the server group created - for the identity. - type: string - userID: - description: UserID is the ID of the user created for the identity. - type: string - required: - - cloud - - cloudConfig - - password - - projectID - - userID - type: object + pause: + description: Pause, if true, will inhibit reconciliation. + type: boolean provider: description: Provider defines the provider type. enum: @@ -116,11 +87,58 @@ spec: - provider type: object status: + properties: + conditions: + description: Current service state of a cluster manager. + items: + description: |- + Condition is a generic condition type for use across all resource types. + It's generic so that the underlying controller-manager functionality can + be shared across all resources. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: Human-readable message indicating details about + last transition. + type: string + reason: + description: Unique, one-word, CamelCase reason for the condition's + last transition. + enum: + - Provisioning + - Provisioned + - Cancelled + - Errored + - Deprovisioning + - Deprovisioned + type: string + status: + description: |- + Status is the status of the condition. + Can be True, False, Unknown. + type: string + type: + description: Type is the type of the condition. + enum: + - Available + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array type: object required: - spec - - status type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml b/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml new file mode 100644 index 0000000..8f232cd --- /dev/null +++ b/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: openstackidentities.region.unikorn-cloud.org +spec: + group: region.unikorn-cloud.org + names: + categories: + - unikorn + kind: OpenstackIdentity + listKind: OpenstackIdentityList + plural: openstackidentities + singular: openstackidentity + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.provider + name: provider + type: string + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: OpenstackIdentity has no controller, its a database record of + state. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + applicationCredentialID: + description: ApplicationCredentialID is the ID of the user's application + credential. + type: string + applicationCredentialSecret: + description: ApplicationCredentialSecret is the one-time secret for + the application credential. + type: string + cloud: + description: Cloud is the cloud name in the cloud config to use. + type: string + cloudConfig: + description: CloudConfig is a client compatible cloud configuration. + format: byte + type: string + password: + description: Password is the login for the user. + type: string + projectID: + description: ProjectID is the ID of the project created for the identity. + type: string + serverGroupID: + description: ServerGroupID is the ID of the server group created for + the identity. + type: string + userID: + description: UserID is the ID of the user created for the identity. + type: string + type: object + status: + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml b/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml index 927c812..4cc5f7f 100644 --- a/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml +++ b/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml @@ -85,7 +85,6 @@ spec: type: object required: - spec - - status type: object served: true storage: true diff --git a/charts/region/templates/_helpers.tpl b/charts/region/templates/_helpers.tpl index 78374be..18bf26d 100644 --- a/charts/region/templates/_helpers.tpl +++ b/charts/region/templates/_helpers.tpl @@ -5,6 +5,10 @@ Create the container images {{- .Values.image | default (printf "%s/unikorn-region-controller:%s" (include "unikorn.defaultRepositoryPath" .) (.Values.tag | default .Chart.Version)) }} {{- end }} +{{- define "unikorn.identityControllerImage" -}} +{{- .Values.identityController.image | default (printf "%s/unikorn-identity-controller:%s" (include "unikorn.defaultRepositoryPath" .) (.Values.tag | default .Chart.Version)) }} +{{- end }} + {{/* Create image pull secrets */}} diff --git a/charts/region/templates/identity-controller/clusterrole.yaml b/charts/region/templates/identity-controller/clusterrole.yaml new file mode 100644 index 0000000..ab41885 --- /dev/null +++ b/charts/region/templates/identity-controller/clusterrole.yaml @@ -0,0 +1,47 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +rules: +# Orchestrate Unikorn resources (my job). +- apiGroups: + - region.unikorn-cloud.org + resources: + - identities + verbs: + - list + - watch + - patch + - update +- apiGroups: + - region.unikorn-cloud.org + resources: + - identities/status + verbs: + - update +- apiGroups: + - region.unikorn-cloud.org + resources: + - openstackidentities + verbs: + - list + - watch + - create + - update + - delete +- apiGroups: + - "" + resources: + - secrets + verbs: + - list + - watch +- apiGroups: + - region.unikorn-cloud.org + resources: + - regions + verbs: + - list + - watch diff --git a/charts/region/templates/identity-controller/clusterrolebinding.yaml b/charts/region/templates/identity-controller/clusterrolebinding.yaml new file mode 100644 index 0000000..435c340 --- /dev/null +++ b/charts/region/templates/identity-controller/clusterrolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + namespace: {{ .Release.Namespace }} + name: unikorn-identity-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: unikorn-identity-controller diff --git a/charts/region/templates/identity-controller/deployment.yaml b/charts/region/templates/identity-controller/deployment.yaml new file mode 100644 index 0000000..3159847 --- /dev/null +++ b/charts/region/templates/identity-controller/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: unikorn-identity-controller + template: + metadata: + labels: + app: unikorn-identity-controller + spec: + containers: + - name: unikorn-identity-controller + image: {{ include "unikorn.identityControllerImage" . }} + args: + {{- include "unikorn.otlp.flags" . | nindent 8 }} + ports: + - name: http + containerPort: 6080 + - name: prometheus + containerPort: 8080 + - name: pprof + containerPort: 6060 + resources: + requests: + cpu: "50m" + memory: 50Mi + limits: + cpu: "100m" + memory: 100Mi + securityContext: + readOnlyRootFilesystem: true + serviceAccountName: unikorn-identity-controller + securityContext: + runAsNonRoot: true diff --git a/charts/region/templates/identity-controller/role.yaml b/charts/region/templates/identity-controller/role.yaml new file mode 100644 index 0000000..0ab2e24 --- /dev/null +++ b/charts/region/templates/identity-controller/role.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +rules: +# Controller prerequisites. +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update +- apiGroups: + - "" + resources: + - events + verbs: + - create + - update diff --git a/charts/region/templates/identity-controller/rolebinding.yaml b/charts/region/templates/identity-controller/rolebinding.yaml new file mode 100644 index 0000000..462ba73 --- /dev/null +++ b/charts/region/templates/identity-controller/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + namespace: {{ .Release.Namespace }} + name: unikorn-identity-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: unikorn-identity-controller diff --git a/charts/region/templates/identity-controller/serviceaccount.yaml b/charts/region/templates/identity-controller/serviceaccount.yaml new file mode 100644 index 0000000..0a61c88 --- /dev/null +++ b/charts/region/templates/identity-controller/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: unikorn-identity-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +{{- with ( include "unikorn.imagePullSecrets" . ) }} +imagePullSecrets: +{{ . }} +{{- end }} diff --git a/charts/region/templates/region-controller/clusterrole.yaml b/charts/region/templates/region-controller/clusterrole.yaml index 729b7f3..dbf86bc 100644 --- a/charts/region/templates/region-controller/clusterrole.yaml +++ b/charts/region/templates/region-controller/clusterrole.yaml @@ -22,6 +22,13 @@ rules: - watch - create - delete +- apiGroups: + - region.unikorn-cloud.org + resources: + - openstackidentities + verbs: + - list + - watch - apiGroups: - "" resources: diff --git a/charts/region/templates/region-controller/ingress.yaml b/charts/region/templates/region-controller/ingress.yaml index fb4f0de..16418c6 100644 --- a/charts/region/templates/region-controller/ingress.yaml +++ b/charts/region/templates/region-controller/ingress.yaml @@ -6,6 +6,11 @@ metadata: {{- include "unikorn.labels" . | nindent 4 }} annotations: {{- include "unikorn.ingress.clusterIssuer.annotations" . | nindent 4 }} + # mTLS is used to authenticate other Unikorn services + nginx.ingress.kubernetes.io/auth-tls-verify-client: optional + nginx.ingress.kubernetes.io/auth-tls-secret: cert-manager/unikorn-client-ca + nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" + nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" {{- if (include "unikorn.ingress.externalDNS" .) }} external-dns.alpha.kubernetes.io/hostname: {{ include "unikorn.region.host" . }} {{- end }} diff --git a/charts/region/values.yaml b/charts/region/values.yaml index 9d9277d..64c49d5 100644 --- a/charts/region/values.yaml +++ b/charts/region/values.yaml @@ -101,6 +101,11 @@ organization: unikorn-cloud # Allows override of the global default image. # image: +# Identity controller configuration. +identityController: + # Allow override of the identity controller image. + image: + # Sets the DNS hosts/X.509 Certs. region: host: region.unikorn-cloud.org diff --git a/cmd/unikorn-identity-controller/main.go b/cmd/unikorn-identity-controller/main.go new file mode 100644 index 0000000..35fbc3a --- /dev/null +++ b/cmd/unikorn-identity-controller/main.go @@ -0,0 +1,27 @@ +/* +Copyright 2022-2024 EscherCloud. +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "github.com/unikorn-cloud/core/pkg/manager" + "github.com/unikorn-cloud/region/pkg/managers/identity" +) + +func main() { + manager.Run(&identity.Factory{}) +} diff --git a/cmd/unikorn-region-controller/main.go b/cmd/unikorn-region-controller/main.go index 3fb942e..e37ccad 100644 --- a/cmd/unikorn-region-controller/main.go +++ b/cmd/unikorn-region-controller/main.go @@ -32,7 +32,6 @@ import ( "github.com/unikorn-cloud/core/pkg/client" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" "github.com/unikorn-cloud/region/pkg/constants" - "github.com/unikorn-cloud/region/pkg/reaper" "github.com/unikorn-cloud/region/pkg/server" "sigs.k8s.io/controller-runtime/pkg/log" @@ -78,12 +77,6 @@ func start() { return } - if err := reaper.New(client, s.Options.Namespace).Run(ctx); err != nil { - logger.Error(err, "failed to setup 'The Reaper'") - - return - } - // Register a signal handler to trigger a graceful shutdown. stop := make(chan os.Signal, 1) diff --git a/docker/unikorn-identity-controller/.dockerignore b/docker/unikorn-identity-controller/.dockerignore new file mode 100644 index 0000000..c5a52a1 --- /dev/null +++ b/docker/unikorn-identity-controller/.dockerignore @@ -0,0 +1,2 @@ +* +!bin/*-linux-gnu/unikorn-identity-controller diff --git a/docker/unikorn-identity-controller/Dockerfile b/docker/unikorn-identity-controller/Dockerfile new file mode 100644 index 0000000..65d7df9 --- /dev/null +++ b/docker/unikorn-identity-controller/Dockerfile @@ -0,0 +1,8 @@ +FROM gcr.io/distroless/static:nonroot + +# This is implcitly created by 'docker buildx build' +ARG TARGETARCH + +COPY bin/${TARGETARCH}-linux-gnu/unikorn-identity-controller / + +ENTRYPOINT ["/unikorn-identity-controller"] diff --git a/docker/unikorn-region-controller/.dockerignore b/docker/unikorn-region-controller/.dockerignore index b6a7519..c556893 100644 --- a/docker/unikorn-region-controller/.dockerignore +++ b/docker/unikorn-region-controller/.dockerignore @@ -1,3 +1,2 @@ * !bin/*-linux-gnu/unikorn-region-controller -!hack/passwd.nonroot diff --git a/docker/unikorn-region-controller/Dockerfile b/docker/unikorn-region-controller/Dockerfile index a54e581..4830e26 100644 --- a/docker/unikorn-region-controller/Dockerfile +++ b/docker/unikorn-region-controller/Dockerfile @@ -3,7 +3,6 @@ FROM gcr.io/distroless/static:nonroot # This is implcitly created by 'docker buildx build' ARG TARGETARCH -# Required as we are talking to Openstack public endpoints. COPY bin/${TARGETARCH}-linux-gnu/unikorn-region-controller / ENTRYPOINT ["/unikorn-region-controller"] diff --git a/go.mod b/go.mod index 20f195b..f789201 100644 --- a/go.mod +++ b/go.mod @@ -10,10 +10,9 @@ require ( github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - github.com/unikorn-cloud/core v0.1.63 - github.com/unikorn-cloud/identity v0.2.29 + github.com/unikorn-cloud/core v0.1.66 + github.com/unikorn-cloud/identity v0.2.30 go.opentelemetry.io/otel v1.28.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 k8s.io/api v0.30.2 @@ -24,12 +23,14 @@ require ( require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/emicklei/go-restful/v3 v3.12.1 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/go-jose/go-jose/v3 v3.0.3 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-jose/go-jose/v4 v4.0.2 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -38,6 +39,7 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -60,7 +62,12 @@ require ( github.com/perimeterx/marshmallow v1.1.5 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_golang v1.19.1 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.28.0 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect @@ -73,6 +80,7 @@ require ( golang.org/x/term v0.22.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect + gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240708141625-4ad9e859172b // indirect google.golang.org/grpc v1.65.0 // indirect diff --git a/go.sum b/go.sum index 39dbfe4..ed75d2c 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/getkin/kin-openapi v0.126.0 h1:c2cSgLnAsS0xYfKsgt5oBV6MYRM/giU8/RtwUY github.com/getkin/kin-openapi v0.126.0/go.mod h1:7mONz8IwmSRg6RttPu6v8U/OJ+gr+J99qSFNjPGSQqw= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= -github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k= -github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ= github.com/go-jose/go-jose/v4 v4.0.2 h1:R3l3kkBds16bO7ZFAEEcofK0MkrAJt3jlJznWZG0nvk= github.com/go-jose/go-jose/v4 v4.0.2/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -133,15 +131,14 @@ github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unikorn-cloud/core v0.1.63 h1:Jl/xuoGRKESMXhS1+apcaS/1I776agTyT75BGz9AKBA= -github.com/unikorn-cloud/core v0.1.63/go.mod h1:JcUIQW3+oiZPUQmOlENw3OCi35IBxPKa+J4MbP3TO7k= -github.com/unikorn-cloud/identity v0.2.29 h1:kKEJmh6tjjdvZWYdZhyRewG3aHf9wmWwG5C/kb+Rm9A= -github.com/unikorn-cloud/identity v0.2.29/go.mod h1:ujrL+6kRUrPIk4Z0Yc12A+FDy6L4b2Hgzz6oGZlKfGI= +github.com/unikorn-cloud/core v0.1.66 h1:5FbosJk2+XsOsvO+nxs7ei/q+5y+uu4PgKtn2W5MnnQ= +github.com/unikorn-cloud/core v0.1.66/go.mod h1:MhA9xeueMB7EJgdyF0rE7lFQSkuHEcoPen6iwt/QCBE= +github.com/unikorn-cloud/identity v0.2.30 h1:geai/ypq393HuVBAPfNk7Gopkig2WK3hNhsm2RuWcKw= +github.com/unikorn-cloud/identity v0.2.30/go.mod h1:GudlMY6lJrlTWlnvHQnpdohK4w79VmuEy2mgT9vkVUM= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -161,6 +158,8 @@ go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeX go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= @@ -170,7 +169,6 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/exp v0.0.0-20240707233637-46b078467d37 h1:uLDX+AfeFCct3a2C7uIWBKMJIR3CJMhcgfrUAqjRK6w= @@ -178,7 +176,6 @@ golang.org/x/exp v0.0.0-20240707233637-46b078467d37/go.mod h1:M4RDyNAINzryxdtnbR golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -186,8 +183,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= @@ -196,7 +191,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -206,16 +200,10 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -223,9 +211,6 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= @@ -235,7 +220,6 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -260,7 +244,6 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.30.2 h1:+ZhRj+28QT4UOH+BKznu4CBgPWgkXO7XAvMcMl0qKvI= diff --git a/pkg/apis/unikorn/v1alpha1/helpers.go b/pkg/apis/unikorn/v1alpha1/helpers.go new file mode 100644 index 0000000..041e222 --- /dev/null +++ b/pkg/apis/unikorn/v1alpha1/helpers.go @@ -0,0 +1,48 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" +) + +// Paused implements the ReconcilePauser interface. +func (c *Identity) Paused() bool { + return c.Spec.Pause +} + +// StatusConditionRead scans the status conditions for an existing condition whose type +// matches. +func (c *Identity) StatusConditionRead(t unikornv1core.ConditionType) (*unikornv1core.Condition, error) { + return unikornv1core.GetCondition(c.Status.Conditions, t) +} + +// StatusConditionWrite either adds or updates a condition in the cluster manager status. +// If the condition, status and message match an existing condition the update is +// ignored. +func (c *Identity) StatusConditionWrite(t unikornv1core.ConditionType, status corev1.ConditionStatus, reason unikornv1core.ConditionReason, message string) { + unikornv1core.UpdateCondition(&c.Status.Conditions, t, status, reason, message) +} + +// ResourceLabels generates a set of labels to uniquely identify the resource +// if it were to be placed in a single global namespace. +func (c *Identity) ResourceLabels() (labels.Set, error) { + return nil, nil +} diff --git a/pkg/apis/unikorn/v1alpha1/register.go b/pkg/apis/unikorn/v1alpha1/register.go index 77de2bb..fde2a74 100644 --- a/pkg/apis/unikorn/v1alpha1/register.go +++ b/pkg/apis/unikorn/v1alpha1/register.go @@ -50,6 +50,7 @@ var ( func init() { SchemeBuilder.Register(&Region{}, &RegionList{}) SchemeBuilder.Register(&Identity{}, &IdentityList{}) + SchemeBuilder.Register(&OpenstackIdentity{}, &OpenstackIdentityList{}) SchemeBuilder.Register(&PhysicalNetwork{}, &PhysicalNetworkList{}) } diff --git a/pkg/apis/unikorn/v1alpha1/types.go b/pkg/apis/unikorn/v1alpha1/types.go index af37331..29d82d9 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -279,6 +279,7 @@ type IdentityList struct { // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="provider",type="string",JSONPath=".spec.provider" // +kubebuilder:printcolumn:name="status",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].reason" // +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" @@ -286,37 +287,66 @@ type Identity struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec IdentitySpec `json:"spec"` - Status IdentityStatus `json:"status"` + Status IdentityStatus `json:"status,omitempty"` } // IdentitySpec stores any state necessary to manage identity. type IdentitySpec struct { + // Pause, if true, will inhibit reconciliation. + Pause bool `json:"pause,omitempty"` // Tags are an abitrary list of key/value pairs that a client // may populate to store metadata for the resource. Tags TagList `json:"tags,omitempty"` // Provider defines the provider type. Provider Provider `json:"provider"` - // OpenStack is populated when the provider type is set to "openstack". - OpenStack *IdentitySpecOpenStack `json:"openstack,omitempty"` } -type IdentitySpecOpenStack struct { +type IdentityStatus struct { + // Current service state of a cluster manager. + Conditions []unikornv1core.Condition `json:"conditions,omitempty"` +} + +// OpenstackIdentityList is a typed list of identities. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type OpenstackIdentityList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenstackIdentity `json:"items"` +} + +// OpenstackIdentity has no controller, its a database record of state. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:printcolumn:name="provider",type="string",JSONPath=".spec.provider" +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +type OpenstackIdentity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec OpenstackIdentitySpec `json:"spec"` + Status OpenstackIdentityStatus `json:"status,omitempty"` +} + +type OpenstackIdentitySpec struct { // CloudConfig is a client compatible cloud configuration. - CloudConfig []byte `json:"cloudConfig"` + CloudConfig []byte `json:"cloudConfig,omitempty"` // Cloud is the cloud name in the cloud config to use. - Cloud string `json:"cloud"` + Cloud *string `json:"cloud,omitempty"` // UserID is the ID of the user created for the identity. - UserID string `json:"userID"` + UserID *string `json:"userID,omitempty"` // Password is the login for the user. - Password string `json:"password"` + Password *string `json:"password,omitempty"` // ProjectID is the ID of the project created for the identity. - ProjectID string `json:"projectID"` + ProjectID *string `json:"projectID,omitempty"` + // ApplicationCredentialID is the ID of the user's application credential. + ApplicationCredentialID *string `json:"applicationCredentialID,omitempty"` + // ApplicationCredentialSecret is the one-time secret for the application credential. + ApplicationCredentialSecret *string `json:"applicationCredentialSecret,omitempty"` // ServerGroupID is the ID of the server group created for the identity. ServerGroupID *string `json:"serverGroupID,omitempty"` } -type IdentityStatus struct { -} +type OpenstackIdentityStatus struct{} // PhysicalNetworkList s a typed list of physical networks. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -336,7 +366,7 @@ type PhysicalNetwork struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` Spec PhysicalNetworkSpec `json:"spec"` - Status PhysicalNetworkStatus `json:"status"` + Status PhysicalNetworkStatus `json:"status,omitempty"` } type PhysicalNetworkSpec struct { diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index 3c0d7d1..4e23872 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -152,7 +152,7 @@ func (in *Identity) DeepCopyInto(out *Identity) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -215,11 +215,6 @@ func (in *IdentitySpec) DeepCopyInto(out *IdentitySpec) { *out = make(TagList, len(*in)) copy(*out, *in) } - if in.OpenStack != nil { - in, out := &in.OpenStack, &out.OpenStack - *out = new(IdentitySpecOpenStack) - (*in).DeepCopyInto(*out) - } return } @@ -233,35 +228,16 @@ func (in *IdentitySpec) DeepCopy() *IdentitySpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IdentitySpecOpenStack) DeepCopyInto(out *IdentitySpecOpenStack) { - *out = *in - if in.CloudConfig != nil { - in, out := &in.CloudConfig, &out.CloudConfig - *out = make([]byte, len(*in)) - copy(*out, *in) - } - if in.ServerGroupID != nil { - in, out := &in.ServerGroupID, &out.ServerGroupID - *out = new(string) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentitySpecOpenStack. -func (in *IdentitySpecOpenStack) DeepCopy() *IdentitySpecOpenStack { - if in == nil { - return nil - } - out := new(IdentitySpecOpenStack) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IdentityStatus) DeepCopyInto(out *IdentityStatus) { *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]unikornv1alpha1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -371,6 +347,139 @@ func (in *OpenstackFlavorsSpec) DeepCopy() *OpenstackFlavorsSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentity) DeepCopyInto(out *OpenstackIdentity) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentity. +func (in *OpenstackIdentity) DeepCopy() *OpenstackIdentity { + if in == nil { + return nil + } + out := new(OpenstackIdentity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackIdentity) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentityList) DeepCopyInto(out *OpenstackIdentityList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenstackIdentity, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentityList. +func (in *OpenstackIdentityList) DeepCopy() *OpenstackIdentityList { + if in == nil { + return nil + } + out := new(OpenstackIdentityList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackIdentityList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentitySpec) DeepCopyInto(out *OpenstackIdentitySpec) { + *out = *in + if in.CloudConfig != nil { + in, out := &in.CloudConfig, &out.CloudConfig + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Cloud != nil { + in, out := &in.Cloud, &out.Cloud + *out = new(string) + **out = **in + } + if in.UserID != nil { + in, out := &in.UserID, &out.UserID + *out = new(string) + **out = **in + } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(string) + **out = **in + } + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(string) + **out = **in + } + if in.ApplicationCredentialID != nil { + in, out := &in.ApplicationCredentialID, &out.ApplicationCredentialID + *out = new(string) + **out = **in + } + if in.ApplicationCredentialSecret != nil { + in, out := &in.ApplicationCredentialSecret, &out.ApplicationCredentialSecret + *out = new(string) + **out = **in + } + if in.ServerGroupID != nil { + in, out := &in.ServerGroupID, &out.ServerGroupID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentitySpec. +func (in *OpenstackIdentitySpec) DeepCopy() *OpenstackIdentitySpec { + if in == nil { + return nil + } + out := new(OpenstackIdentitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentityStatus) DeepCopyInto(out *OpenstackIdentityStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentityStatus. +func (in *OpenstackIdentityStatus) DeepCopy() *OpenstackIdentityStatus { + if in == nil { + return nil + } + out := new(OpenstackIdentityStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenstackProviderNetworkSpec) DeepCopyInto(out *OpenstackProviderNetworkSpec) { *out = *in diff --git a/pkg/client/client.go b/pkg/client/client.go index 70cd864..48d2dd7 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,36 +18,24 @@ package client import ( "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" "net/http" - "github.com/spf13/pflag" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" - "github.com/unikorn-cloud/core/pkg/authorization/accesstoken" + coreclient "github.com/unikorn-cloud/core/pkg/client" + "github.com/unikorn-cloud/identity/pkg/middleware/authorization" + "github.com/unikorn-cloud/identity/pkg/middleware/openapi/accesstoken" "github.com/unikorn-cloud/region/pkg/openapi" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) -var ( - // ErrFormatError is returned when a secret doesn't meet the specification. - ErrFormatError = errors.New("secret incorrectly formatted") -) +type Options = coreclient.HTTPOptions -type Options struct { - // host is the region host name. - host string - // caSecretNamespace tells us where to source the CA secret. - caSecretNamespace string - // caSecretName is the root CA secret of the region endpoint. - caSecretName string +// NewOptions must be used to create options for consistency. +func NewOptions() *Options { + return coreclient.NewHTTPOptions("region") } // Client wraps up the raw OpenAPI client with things to make it useable e.g. @@ -55,77 +43,28 @@ type Options struct { type Client struct { // client is a Kubenetes client. client client.Client - // namespace is the namespace the client is running in. - namespace string // options allows setting of option from the CLI options *Options -} - -// AddFlags adds the options to the CLI flags. -func (o *Options) AddFlags(f *pflag.FlagSet) { - f.StringVar(&o.host, "region-host", "", "Region endpoint URL.") - f.StringVar(&o.caSecretNamespace, "region-ca-secret-namespace", "", "Region endpoint CA certificate secret namespace.") - f.StringVar(&o.caSecretName, "region-ca-secret-name", "", "Region endpoint CA certificate secret.") + // clientOptions may be specified to inject client certificates etc. + clientOptions *coreclient.HTTPClientOptions } // New creates a new client. -func New(client client.Client, namespace string, options *Options) *Client { +func New(client client.Client, options *Options, clientOptions *coreclient.HTTPClientOptions) *Client { return &Client{ - client: client, - namespace: namespace, - options: options, - } -} - -// tlsClientConfig abstracts away private TLS CAs or self signed certificates. -func (c *Client) tlsClientConfig(ctx context.Context) (*tls.Config, error) { - if c.options.caSecretName == "" { - //nolint:nilnil - return nil, nil - } - - namespace := c.namespace - - if c.options.caSecretNamespace != "" { - namespace = c.options.caSecretNamespace - } - - secret := &corev1.Secret{} - - if err := c.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: c.options.caSecretName}, secret); err != nil { - return nil, err - } - - if secret.Type != corev1.SecretTypeTLS { - return nil, fmt.Errorf("%w: issuer CA not of type kubernetes.io/tls", ErrFormatError) + client: client, + options: options, + clientOptions: clientOptions, } - - cert, ok := secret.Data[corev1.TLSCertKey] - if !ok { - return nil, fmt.Errorf("%w: issuer CA missing tls.crt", ErrFormatError) - } - - certPool := x509.NewCertPool() - - if ok := certPool.AppendCertsFromPEM(cert); !ok { - return nil, fmt.Errorf("%w: failed to load region CA certificate", ErrFormatError) - } - - config := &tls.Config{ - RootCAs: certPool, - MinVersion: tls.VersionTLS13, - } - - return config, nil } -// httpClient returns a new http client that will transparently do oauth2 header +// HTTPClient returns a new http client that will transparently do oauth2 header // injection and refresh token updates. -func (c *Client) httpClient(ctx context.Context) (*http.Client, error) { +func (c *Client) HTTPClient(ctx context.Context) (*http.Client, error) { // Handle non-system CA certificates for the OIDC discovery protocol // and oauth2 token refresh. This will return nil if none is specified // and default to the system roots. - tlsClientConfig, err := c.tlsClientConfig(ctx) + tlsClientConfig, err := coreclient.TLSClientConfig(ctx, c.client, c.options, c.clientOptions) if err != nil { return nil, err } @@ -141,21 +80,26 @@ func (c *Client) httpClient(ctx context.Context) (*http.Client, error) { // accessTokenInjector implements OAuth2 bearer token authorization. func accessTokenInjector(ctx context.Context, req *http.Request) error { - req.Header.Set("Authorization", "bearer "+accesstoken.FromContext(ctx)) + accessToken, err := accesstoken.FromContext(ctx) + if err != nil { + return err + } + req.Header.Set("Authorization", "bearer "+accessToken) otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + authorization.InjectClientCert(ctx, req.Header) return nil } // Client returns a new OpenAPI client that can be used to access the API. func (c *Client) Client(ctx context.Context) (*openapi.ClientWithResponses, error) { - httpClient, err := c.httpClient(ctx) + httpClient, err := c.HTTPClient(ctx) if err != nil { return nil, err } - client, err := openapi.NewClientWithResponses(c.options.host, openapi.WithHTTPClient(httpClient), openapi.WithRequestEditorFn(accessTokenInjector)) + client, err := openapi.NewClientWithResponses(c.options.Host(), openapi.WithHTTPClient(httpClient), openapi.WithRequestEditorFn(accessTokenInjector)) if err != nil { return nil, err } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 8488cbb..651bc42 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -27,12 +27,14 @@ import ( "slices" "time" + unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" coreconstants "github.com/unikorn-cloud/core/pkg/constants" coreapi "github.com/unikorn-cloud/core/pkg/openapi" "github.com/unikorn-cloud/core/pkg/server/conversion" "github.com/unikorn-cloud/core/pkg/server/errors" coreutil "github.com/unikorn-cloud/core/pkg/util" identityclient "github.com/unikorn-cloud/identity/pkg/client" + "github.com/unikorn-cloud/identity/pkg/middleware/authorization" identityapi "github.com/unikorn-cloud/identity/pkg/openapi" "github.com/unikorn-cloud/identity/pkg/rbac" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" @@ -297,9 +299,15 @@ func convertTags(in unikornv1.TagList) openapi.TagList { return out } -func convertIdentity(in *unikornv1.Identity) *openapi.IdentityRead { +func (h *Handler) convertIdentity(ctx context.Context, in *unikornv1.Identity) *openapi.IdentityRead { + provisioningStatus := coreapi.ResourceProvisioningStatusUnknown + + if condition, err := in.StatusConditionRead(unikornv1core.ConditionAvailable); err == nil { + provisioningStatus = conversion.ConvertStatusCondition(condition) + } + out := &openapi.IdentityRead{ - Metadata: conversion.ProjectScopedResourceReadMetadata(in, coreapi.ResourceProvisioningStatusProvisioned), + Metadata: conversion.ProjectScopedResourceReadMetadata(in, provisioningStatus), Spec: openapi.IdentitySpec{ RegionId: in.Labels[constants.RegionLabel], }, @@ -313,28 +321,31 @@ func convertIdentity(in *unikornv1.Identity) *openapi.IdentityRead { case unikornv1.ProviderOpenstack: out.Spec.Type = openapi.Openstack - cloudConfig := base64.URLEncoding.EncodeToString(in.Spec.OpenStack.CloudConfig) + var openstackIdentity unikornv1.OpenstackIdentity - out.Spec.Openstack = &openapi.IdentitySpecOpenStack{ - CloudConfig: cloudConfig, - Cloud: in.Spec.OpenStack.Cloud, - UserId: in.Spec.OpenStack.UserID, - ProjectId: in.Spec.OpenStack.ProjectID, - } + if err := h.client.Get(ctx, client.ObjectKey{Namespace: in.Namespace, Name: in.Name}, &openstackIdentity); err == nil { + out.Spec.Openstack = &openapi.IdentitySpecOpenStack{ + Cloud: openstackIdentity.Spec.Cloud, + UserId: openstackIdentity.Spec.UserID, + ProjectId: openstackIdentity.Spec.ProjectID, + ServerGroupId: openstackIdentity.Spec.ServerGroupID, + } - if in.Spec.OpenStack.ServerGroupID != nil { - out.Spec.Openstack.ServerGroupId = in.Spec.OpenStack.ServerGroupID + if openstackIdentity.Spec.CloudConfig != nil { + cloudConfig := base64.URLEncoding.EncodeToString(openstackIdentity.Spec.CloudConfig) + out.Spec.Openstack.CloudConfig = &cloudConfig + } } } return out } -func convertIdentityList(in unikornv1.IdentityList) openapi.IdentitiesRead { +func (h *Handler) convertIdentityList(ctx context.Context, in unikornv1.IdentityList) openapi.IdentitiesRead { out := make(openapi.IdentitiesRead, len(in.Items)) for i := range in.Items { - out[i] = *convertIdentity(&in.Items[i]) + out[i] = *h.convertIdentity(ctx, &in.Items[i]) } return out @@ -363,7 +374,30 @@ func (h *Handler) GetApiV1OrganizationsOrganizationIDIdentities(w http.ResponseW return cmp.Compare(a.Name, b.Name) }) - util.WriteJSONResponse(w, r, http.StatusOK, convertIdentityList(result)) + util.WriteJSONResponse(w, r, http.StatusOK, h.convertIdentityList(r.Context(), result)) +} + +func generateTag(in openapi.Tag) unikornv1.Tag { + out := unikornv1.Tag{ + Name: in.Name, + Value: in.Value, + } + + return out +} + +func generateTagList(in *openapi.TagList) unikornv1.TagList { + if in == nil { + return nil + } + + out := make(unikornv1.TagList, len(*in)) + + for i := range *in { + out[i] = generateTag((*in)[i]) + } + + return out } func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentities(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter) { @@ -381,22 +415,40 @@ func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitie provider, err := region.NewClient(h.client, h.namespace).Provider(r.Context(), request.Spec.RegionId) if err != nil { - errors.HandleError(w, r, err) + errors.HandleError(w, r, errors.OAuth2ServerError("unable to get region provider").WithError(err)) return } - identity, err := provider.CreateIdentity(r.Context(), organizationID, projectID, request) + region, err := provider.Region(r.Context()) if err != nil { - errors.HandleError(w, r, err) + errors.HandleError(w, r, errors.OAuth2ServerError("unable to get region").WithError(err)) return } - h.setCacheable(w) - util.WriteJSONResponse(w, r, http.StatusCreated, convertIdentity(identity)) + userinfo, err := authorization.UserinfoFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, errors.OAuth2ServerError("unable to get userinfo").WithError(err)) + return + } + + identity := &unikornv1.Identity{ + ObjectMeta: conversion.NewObjectMetadata(&request.Metadata, h.namespace, userinfo.Sub).WithOrganization(organizationID).WithProject(projectID).WithLabel(constants.RegionLabel, request.Spec.RegionId).Get(), + Spec: unikornv1.IdentitySpec{ + Tags: generateTagList(request.Spec.Tags), + Provider: region.Spec.Provider, + }, + } + + if err := h.client.Create(r.Context(), identity); err != nil { + errors.HandleError(w, r, errors.OAuth2ServerError("unable to create identity").WithError(err)) + return + } + + util.WriteJSONResponse(w, r, http.StatusCreated, h.convertIdentity(r.Context(), identity)) } -func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { - if err := rbac.AllowProjectScope(r.Context(), "identities", identityapi.Delete, organizationID, projectID); err != nil { +func (h *Handler) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { + if err := rbac.AllowProjectScope(r.Context(), "identities", identityapi.Read, organizationID, projectID); err != nil { errors.HandleError(w, r, err) return } @@ -407,14 +459,28 @@ func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentit return } - provider, err := region.NewClient(h.client, h.namespace).Provider(r.Context(), identity.Labels[constants.RegionLabel]) + util.WriteJSONResponse(w, r, http.StatusOK, h.convertIdentity(r.Context(), identity)) +} + +func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { + if err := rbac.AllowProjectScope(r.Context(), "identities", identityapi.Delete, organizationID, projectID); err != nil { + errors.HandleError(w, r, err) + return + } + + identity, err := h.getIdentity(r.Context(), identityID) if err != nil { errors.HandleError(w, r, err) return } - if err := provider.DeleteIdentity(r.Context(), identity); err != nil { - errors.HandleError(w, r, errors.OAuth2ServerError("failed to delete identity").WithError(err)) + if err := h.client.Delete(r.Context(), identity); err != nil { + if kerrors.IsNotFound(err) { + errors.HandleError(w, r, errors.HTTPNotFound().WithError(err)) + return + } + + errors.HandleError(w, r, errors.OAuth2ServerError("unable to delete identity").WithError(err)) return } diff --git a/pkg/managers/identity/manager.go b/pkg/managers/identity/manager.go new file mode 100644 index 0000000..b3e5bf2 --- /dev/null +++ b/pkg/managers/identity/manager.go @@ -0,0 +1,79 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package identity + +import ( + coreclient "github.com/unikorn-cloud/core/pkg/client" + coremanager "github.com/unikorn-cloud/core/pkg/manager" + "github.com/unikorn-cloud/core/pkg/manager/options" + unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" + "github.com/unikorn-cloud/region/pkg/constants" + "github.com/unikorn-cloud/region/pkg/provisioners/managers/identity" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" +) + +// Factory provides methods that can build a type specific controller. +type Factory struct{} + +var _ coremanager.ControllerFactory = &Factory{} + +// Metadata returns the application, version and revision. +func (*Factory) Metadata() (string, string, string) { + return constants.Application, constants.Version, constants.Revision +} + +// Options returns any options to be added to the CLI flags and passed to the reconciler. +func (*Factory) Options() coremanager.ControllerOptions { + return nil +} + +// Reconciler returns a new reconciler instance. +func (*Factory) Reconciler(options *options.Options, controllerOptions coremanager.ControllerOptions, manager manager.Manager) reconcile.Reconciler { + return coremanager.NewReconciler(options, controllerOptions, manager, identity.New) +} + +// RegisterWatches adds any watches that would trigger a reconcile. +func (*Factory) RegisterWatches(manager manager.Manager, controller controller.Controller) error { + // Any changes to the identity spec, trigger a reconcile. + if err := controller.Watch(source.Kind(manager.GetCache(), &unikornv1.Identity{}, &handler.TypedEnqueueRequestForObject[*unikornv1.Identity]{}, &predicate.TypedGenerationChangedPredicate[*unikornv1.Identity]{})); err != nil { + return err + } + + return nil +} + +// Upgrade can perform metadata upgrades of all versioned resources on restart/upgrade +// of the controller. This must not affect the spec in any way as it causes split brain +// and potential fail. +func (*Factory) Upgrade(_ client.Client) error { + return nil +} + +// Schemes allows controllers to add types to the client beyond +// the defaults defined in this repository. +func (*Factory) Schemes() []coreclient.SchemeAdder { + return []coreclient.SchemeAdder{ + unikornv1.AddToScheme, + } +} diff --git a/pkg/openapi/client.go b/pkg/openapi/client.go index 3fb0817..d0c6baa 100644 --- a/pkg/openapi/client.go +++ b/pkg/openapi/client.go @@ -101,6 +101,9 @@ type ClientInterface interface { // DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID request DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID request + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBody request with any body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -167,6 +170,18 @@ func (c *Client) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentiti return c.Client.Do(req) } +func (c *Client) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDRequest(c.Server, organizationID, projectID, identityID) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(c.Server, organizationID, projectID, identityID, contentType, body) if err != nil { @@ -375,6 +390,54 @@ func NewDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentit return req, nil } +// NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDRequest generates requests for GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID +func NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "organizationID", runtime.ParamLocationPath, organizationID) + if err != nil { + return nil, err + } + + var pathParam1 string + + pathParam1, err = runtime.StyleParamWithLocation("simple", false, "projectID", runtime.ParamLocationPath, projectID) + if err != nil { + return nil, err + } + + var pathParam2 string + + pathParam2, err = runtime.StyleParamWithLocation("simple", false, "identityID", runtime.ParamLocationPath, identityID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/organizations/%s/projects/%s/identities/%s", pathParam0, pathParam1, pathParam2) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequest calls the generic PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks builder with application/json body func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -647,6 +710,9 @@ type ClientWithResponsesInterface interface { // DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse request DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse, error) + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse request + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse, error) + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse request with any body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) @@ -743,6 +809,32 @@ func (r DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentit return 0 } +type GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *IdentityResponse + JSON400 *externalRef0.BadRequestResponse + JSON401 *externalRef0.UnauthorizedResponse + JSON403 *externalRef0.ForbiddenResponse + JSON500 *externalRef0.InternalServerErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse struct { Body []byte HTTPResponse *http.Response @@ -904,6 +996,15 @@ func (c *ClientWithResponses) DeleteApiV1OrganizationsOrganizationIDProjectsProj return ParseDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse(rsp) } +// GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse request returning *GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse +func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse, error) { + rsp, err := c.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(ctx, organizationID, projectID, identityID, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse(rsp) +} + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse request with arbitrary body returning *PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { rsp, err := c.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx, organizationID, projectID, identityID, contentType, body, reqEditors...) @@ -1119,6 +1220,60 @@ func ParseDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdent return response, nil } +// ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDWithResponse call +func ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest IdentityResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest externalRef0.BadRequestResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest externalRef0.UnauthorizedResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 403: + var dest externalRef0.ForbiddenResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON403 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest externalRef0.InternalServerErrorResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse parses an HTTP response from a PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithResponse call func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp *http.Response) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/openapi/router.go b/pkg/openapi/router.go index dd68832..1a03c51 100644 --- a/pkg/openapi/router.go +++ b/pkg/openapi/router.go @@ -24,6 +24,9 @@ type ServerInterface interface { // (DELETE /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) + // (GET /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}) + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) + // (POST /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalNetworks) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) @@ -59,6 +62,11 @@ func (_ Unimplemented) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDId w.WriteHeader(http.StatusNotImplemented) } +// (GET /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}) +func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) { + w.WriteHeader(http.StatusNotImplemented) +} + // (POST /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalNetworks) func (_ Unimplemented) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) { w.WriteHeader(http.StatusNotImplemented) @@ -204,6 +212,52 @@ func (siw *ServerInterfaceWrapper) DeleteApiV1OrganizationsOrganizationIDProject handler.ServeHTTP(w, r.WithContext(ctx)) } +// GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID operation middleware +func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request) { + ctx := r.Context() + + var err error + + // ------------- Path parameter "organizationID" ------------- + var organizationID OrganizationIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "organizationID", runtime.ParamLocationPath, chi.URLParam(r, "organizationID"), &organizationID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "organizationID", Err: err}) + return + } + + // ------------- Path parameter "projectID" ------------- + var projectID ProjectIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "projectID", runtime.ParamLocationPath, chi.URLParam(r, "projectID"), &projectID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "projectID", Err: err}) + return + } + + // ------------- Path parameter "identityID" ------------- + var identityID IdentityIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "identityID", runtime.ParamLocationPath, chi.URLParam(r, "identityID"), &identityID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "identityID", Err: err}) + return + } + + ctx = context.WithValue(ctx, Oauth2AuthenticationScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w, r, organizationID, projectID, identityID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks operation middleware func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -511,6 +565,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Delete(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}", wrapper.DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID) }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}", wrapper.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID) + }) r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalNetworks", wrapper.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks) }) diff --git a/pkg/openapi/schema.go b/pkg/openapi/schema.go index 1dd280d..1eb12f9 100644 --- a/pkg/openapi/schema.go +++ b/pkg/openapi/schema.go @@ -19,96 +19,96 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/pkg/openapi/server.spec.yaml b/pkg/openapi/server.spec.yaml index ebbf87a..5fab2d1 100644 --- a/pkg/openapi/server.spec.yaml +++ b/pkg/openapi/server.spec.yaml @@ -145,6 +145,21 @@ paths: - $ref: '#/components/parameters/organizationIDParameter' - $ref: '#/components/parameters/projectIDParameter' - $ref: '#/components/parameters/identityIDParameter' + get: + description: Get a single identity. + security: + - oauth2Authentication: [] + responses: + '200': + $ref: '#/components/responses/identityResponse' + '400': + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/badRequestResponse' + '401': + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/unauthorizedResponse' + '403': + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/forbiddenResponse' + '500': + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/internalServerErrorResponse' delete: description: Delete an identity and any resources associated with it. security: @@ -445,11 +460,6 @@ components: identitySpecOpenStack: description: Everything an OpenStack client needs to function. type: object - required: - - cloud - - cloudConfig - - userId - - projectId properties: cloud: description: The name of the cloud in the cloud config. @@ -555,7 +565,6 @@ components: $ref: '#/components/schemas/identityWrite' example: metadata: - id: c7568e2d-f9ab-453d-9a3a-51375f78426b name: identity-name description: A verbose description spec: @@ -571,7 +580,6 @@ components: $ref: '#/components/schemas/physicalNetworkWrite' example: metadata: - id: c7568e2d-f9ab-453d-9a3a-51375f78426b name: physical-network-name description: A verbose description spec: diff --git a/pkg/openapi/types.go b/pkg/openapi/types.go index 442b82f..cd9a2ba 100644 --- a/pkg/openapi/types.go +++ b/pkg/openapi/types.go @@ -133,19 +133,19 @@ type IdentitySpec struct { // IdentitySpecOpenStack Everything an OpenStack client needs to function. type IdentitySpecOpenStack struct { // Cloud The name of the cloud in the cloud config. - Cloud string `json:"cloud"` + Cloud *string `json:"cloud,omitempty"` // CloudConfig A base64 encoded cloud config file. - CloudConfig string `json:"cloudConfig"` + CloudConfig *string `json:"cloudConfig,omitempty"` // ProjectId Project identifier allocated for the infrastructure. - ProjectId string `json:"projectId"` + ProjectId *string `json:"projectId,omitempty"` // ServerGroupId Server group identifier allocated for the intrastructure. ServerGroupId *string `json:"serverGroupId,omitempty"` // UserId User identitifer allocated for the infrastructure. - UserId string `json:"userId"` + UserId *string `json:"userId,omitempty"` } // IdentityWrite An identity request. diff --git a/pkg/providers/interfaces.go b/pkg/providers/interfaces.go index 4bdcf80..2770504 100644 --- a/pkg/providers/interfaces.go +++ b/pkg/providers/interfaces.go @@ -27,12 +27,14 @@ import ( // They are also expected to provide any caching or memoization required // to provide high performance and a decent UX. type Provider interface { + // Region returns the provider's region. + Region(ctx context.Context) (*unikornv1.Region, error) // Flavors list all available flavors. Flavors(ctx context.Context) (FlavorList, error) // Images lists all available images. Images(ctx context.Context) (ImageList, error) // CreateIdentity creates a new identity for cloud infrastructure. - CreateIdentity(ctx context.Context, organizationID, projectID string, request *openapi.IdentityWrite) (*unikornv1.Identity, error) + CreateIdentity(ctx context.Context, identity *unikornv1.Identity) error // DeleteIdentity cleans up an identity for cloud infrastructure. DeleteIdentity(ctx context.Context, identity *unikornv1.Identity) error // CreatePhysicalNetwork create a new physical network. diff --git a/pkg/providers/openstack/identity.go b/pkg/providers/openstack/identity.go index b43c42b..8c05072 100644 --- a/pkg/providers/openstack/identity.go +++ b/pkg/providers/openstack/identity.go @@ -327,16 +327,6 @@ func (c *IdentityClient) CreateUser(ctx context.Context, domainID, name, passwor return users.Create(ctx, c.client, opts).Extract() } -// GetUser returns user details. -func (c *IdentityClient) GetUser(ctx context.Context, userID string) (*users.User, error) { - tracer := otel.GetTracerProvider().Tracer(constants.Application) - - _, span := tracer.Start(ctx, "GET /identity/v3/users/"+userID, trace.WithSpanKind(trace.SpanKindClient)) - defer span.End() - - return users.Get(ctx, c.client, userID).Extract() -} - // DeleteUser removes an existing user. func (c *IdentityClient) DeleteUser(ctx context.Context, userID string) error { tracer := otel.GetTracerProvider().Tracer(constants.Application) diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index 671b0c8..dd88e70 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -25,25 +25,25 @@ import ( "strings" "sync" - "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/applicationcredentials" - "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/projects" "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/roles" - "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" "github.com/gophercloud/utils/openstack/clientconfig" coreconstants "github.com/unikorn-cloud/core/pkg/constants" "github.com/unikorn-cloud/core/pkg/server/conversion" + "github.com/unikorn-cloud/identity/pkg/middleware/authorization" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" "github.com/unikorn-cloud/region/pkg/constants" "github.com/unikorn-cloud/region/pkg/openapi" "github.com/unikorn-cloud/region/pkg/providers" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/util/rand" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/uuid" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) @@ -234,6 +234,19 @@ func (p *Provider) network(ctx context.Context) (*NetworkClient, error) { return p._network, nil } +// Region returns the provider's region. +func (p *Provider) Region(ctx context.Context) (*unikornv1.Region, error) { + // Get the newest version of the region. + p.lock.Lock() + defer p.lock.Unlock() + + if err := p.serviceClientRefresh(ctx); err != nil { + return nil, err + } + + return p.region, nil +} + // Flavors list all available flavors. func (p *Provider) Flavors(ctx context.Context) (providers.FlavorList, error) { computeService, err := p.compute(ctx) @@ -359,41 +372,59 @@ const ( ) // projectTags defines how to tag projects. -func projectTags(organizationID, projectID string) []string { +func projectTags(identity *unikornv1.OpenstackIdentity) []string { tags := []string{ - OrganizationTag + "=" + organizationID, - ProjectTag + "=" + projectID, + OrganizationTag + "=" + identity.Labels[coreconstants.OrganizationLabel], + ProjectTag + "=" + identity.Labels[coreconstants.ProjectLabel], } return tags } +func identityResourceName(identity *unikornv1.OpenstackIdentity) string { + return "unikorn-identity-" + identity.Name +} + // provisionUser creates a new user in the managed domain with a random password. // There is a 1:1 mapping of user to project, and the project name is unique in the // domain, so just reuse this, we can clean them up at the same time. -func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityClient, project *projects.Project) (*users.User, string, error) { +func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.UserID != nil { + return nil + } + + name := identityResourceName(identity) password := string(uuid.NewUUID()) - user, err := identityService.CreateUser(ctx, p.domainID, project.Name, password) + user, err := identityService.CreateUser(ctx, p.domainID, name, password) if err != nil { - return nil, "", err + return err } - return user, password, nil + identity.Spec.UserID = &user.ID + identity.Spec.Password = &password + + return nil } // provisionProject creates a project per-cluster. Cluster API provider Openstack is // somewhat broken in that networks can alias and cause all kinds of disasters, so it's // safest to have one cluster in one project so it has its own namespace. -func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, organizationID, projectID string) (*projects.Project, error) { - name := "unikorn-" + rand.String(8) +func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ProjectID != nil { + return nil + } + + name := identityResourceName(identity) - project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(organizationID, projectID)) + project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(identity)) if err != nil { - return nil, err + return err } - return project, nil + identity.Spec.ProjectID = &project.ID + + return nil } // roleNameToID maps from something human readable to something Openstack will operate with @@ -426,7 +457,7 @@ func (p *Provider) getRequiredRoles() []string { // provisionProjectRoles creates a binding between our service account and the project // with the required roles to provision an application credential that will allow cluster // creation, deletion and life-cycle management. -func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, userID string, project *projects.Project) error { +func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { allRoles, err := identityService.ListRoles(ctx) if err != nil { return err @@ -438,7 +469,7 @@ func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *I return err } - if err := identityService.CreateRoleAssignment(ctx, userID, project.ID, roleID); err != nil { + if err := identityService.CreateRoleAssignment(ctx, *identity.Spec.UserID, *identity.Spec.ProjectID, roleID); err != nil { return err } } @@ -446,21 +477,37 @@ func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *I return nil } -func (p *Provider) provisionApplicationCredential(ctx context.Context, userID, password string, project *projects.Project) (*applicationcredentials.ApplicationCredential, error) { +func (p *Provider) provisionApplicationCredential(ctx context.Context, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ApplicationCredentialID != nil { + return nil + } + // Rescope to the user/project... - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, userID, password, project.ID) + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.UserID, *identity.Spec.Password, *identity.Spec.ProjectID) identityService, err := NewIdentityClient(ctx, providerClient) if err != nil { - return nil, err + return err + } + + name := identityResourceName(identity) + + appcred, err := identityService.CreateApplicationCredential(ctx, *identity.Spec.UserID, name, "IaaS lifecycle management", p.getRequiredRoles()) + if err != nil { + return err } - // Application crdentials are scoped to the user, not the project, so the name needs - // to be unique, so just use the project name. - return identityService.CreateApplicationCredential(ctx, userID, project.Name, "IaaS lifecycle management", p.getRequiredRoles()) + identity.Spec.ApplicationCredentialID = &appcred.ID + identity.Spec.ApplicationCredentialSecret = &appcred.Secret + + return nil } -func (p *Provider) createClientConfig(applicationCredential *applicationcredentials.ApplicationCredential) ([]byte, string, error) { +func (p *Provider) createClientConfig(identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.Cloud != nil { + return nil + } + cloud := "cloud" clientConfig := &clientconfig.Clouds{ @@ -469,8 +516,8 @@ func (p *Provider) createClientConfig(applicationCredential *applicationcredenti AuthType: clientconfig.AuthV3ApplicationCredential, AuthInfo: &clientconfig.AuthInfo{ AuthURL: p.region.Spec.Openstack.Endpoint, - ApplicationCredentialID: applicationCredential.ID, - ApplicationCredentialSecret: applicationCredential.Secret, + ApplicationCredentialID: *identity.Spec.ApplicationCredentialID, + ApplicationCredentialSecret: *identity.Spec.ApplicationCredentialSecret, }, }, }, @@ -478,10 +525,13 @@ func (p *Provider) createClientConfig(applicationCredential *applicationcredenti clientConfigYAML, err := yaml.Marshal(clientConfig) if err != nil { - return nil, "", err + return err } - return clientConfigYAML, cloud, nil + identity.Spec.Cloud = &cloud + identity.Spec.CloudConfig = clientConfigYAML + + return nil } func convertTag(in openapi.Tag) unikornv1.Tag { @@ -507,113 +557,163 @@ func convertTagList(in *openapi.TagList) unikornv1.TagList { return out } -func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unikornv1.Identity, userID, password string) error { +func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ServerGroupID != nil { + return nil + } + // Rescope to the user/project... - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, userID, password, identity.Spec.OpenStack.ProjectID) + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.UserID, *identity.Spec.Password, *identity.Spec.ProjectID) computeService, err := NewComputeClient(ctx, providerClient, p.region.Spec.Openstack.Compute) if err != nil { return err } - result, err := computeService.CreateServerGroup(ctx, "cluster-anti-afinity") + name := identityResourceName(identity) + + result, err := computeService.CreateServerGroup(ctx, name) if err != nil { return err } - identity.Spec.OpenStack.ServerGroupID = &result.ID + identity.Spec.ServerGroupID = &result.ID return nil } +func (p *Provider) GetOpenstackIdentity(ctx context.Context, identity *unikornv1.Identity) (*unikornv1.OpenstackIdentity, error) { + var result unikornv1.OpenstackIdentity + + if err := p.client.Get(ctx, client.ObjectKey{Namespace: identity.Namespace, Name: identity.Name}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (p *Provider) GetOrCreateOpenstackIdentity(ctx context.Context, identity *unikornv1.Identity) (*unikornv1.OpenstackIdentity, bool, error) { + create := false + + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) + if err != nil { + if !kerrors.IsNotFound(err) { + return nil, false, err + } + + openstackIdentity = &unikornv1.OpenstackIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: identity.Namespace, + Name: identity.Name, + Labels: map[string]string{ + constants.IdentityLabel: identity.Name, + }, + Annotations: identity.Annotations, + }, + } + + for k, v := range identity.Labels { + openstackIdentity.Labels[k] = v + } + + create = true + } + + return openstackIdentity, create, nil +} + // CreateIdentity creates a new identity for cloud infrastructure. -// -//nolint:cyclop -func (p *Provider) CreateIdentity(ctx context.Context, organizationID, projectID string, request *openapi.IdentityWrite) (*unikornv1.Identity, error) { +func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Identity) error { identityService, err := p.identity(ctx) if err != nil { - return nil, err + return err } + openstackIdentity, create, err := p.GetOrCreateOpenstackIdentity(ctx, identity) + if err != nil { + return err + } + + record := func() { + log := log.FromContext(ctx) + + if create { + if err := p.client.Create(ctx, openstackIdentity); err != nil { + log.Error(err, "failed to create openstack identity") + } + + return + } + + if err := p.client.Update(ctx, openstackIdentity); err != nil { + log.Error(err, "failed to update openstack identity") + } + } + + defer record() + // Every cluster has its own project to mitigate "nuances" in CAPO i.e. it's // totally broken when it comes to network aliasing. - project, err := p.provisionProject(ctx, identityService, organizationID, projectID) - if err != nil { - return nil, err + if err := p.provisionProject(ctx, identityService, openstackIdentity); err != nil { + return err } // You MUST provision a new user, if we rotate a password, any application credentials // hanging off it will stop working, i.e. doing that to the unikorn management user // will be pretty catastrophic for all clusters in the region. - user, password, err := p.provisionUser(ctx, identityService, project) - if err != nil { - return nil, err + if err := p.provisionUser(ctx, identityService, openstackIdentity); err != nil { + return err } // Give the user only what permissions they need to provision a cluster and // manage it during its lifetime. - if err := p.provisionProjectRoles(ctx, identityService, user.ID, project); err != nil { - return nil, err + if err := p.provisionProjectRoles(ctx, identityService, openstackIdentity); err != nil { + return err } // Always use application credentials, they are scoped to a single project and // cannot be used to break from that jail. - applicationCredential, err := p.provisionApplicationCredential(ctx, user.ID, password, project) - if err != nil { - return nil, err - } - - cloudConfig, cloud, err := p.createClientConfig(applicationCredential) - if err != nil { - return nil, err + if err := p.provisionApplicationCredential(ctx, openstackIdentity); err != nil { + return err } - objectMeta := conversion.NewObjectMetadata(&request.Metadata, p.region.Namespace) - objectMeta = objectMeta.WithOrganization(organizationID) - objectMeta = objectMeta.WithProject(projectID) - objectMeta = objectMeta.WithLabel(constants.RegionLabel, p.region.Name) - - identity := &unikornv1.Identity{ - ObjectMeta: objectMeta.Get(ctx), - Spec: unikornv1.IdentitySpec{ - Tags: convertTagList(request.Spec.Tags), - Provider: unikornv1.ProviderOpenstack, - OpenStack: &unikornv1.IdentitySpecOpenStack{ - CloudConfig: cloudConfig, - Cloud: cloud, - UserID: user.ID, - Password: password, - ProjectID: project.ID, - }, - }, + if err := p.createClientConfig(openstackIdentity); err != nil { + return err } // Add in any optional configuration. - if err := p.createIdentityServerGroup(ctx, identity, user.ID, password); err != nil { - return nil, err - } - - if err := p.client.Create(ctx, identity); err != nil { - return nil, err + if err := p.createIdentityServerGroup(ctx, openstackIdentity); err != nil { + return err } - return identity, nil + return nil } // DeleteIdentity cleans up an identity for cloud infrastructure. func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Identity) error { - // Rescope to the user/project... - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, identity.Spec.OpenStack.UserID, identity.Spec.OpenStack.Password, identity.Spec.OpenStack.ProjectID) - - computeService, err := NewComputeClient(ctx, providerClient, p.region.Spec.Openstack.Compute) + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) if err != nil { - return err + if !kerrors.IsNotFound(err) { + return err + } + + return nil } - if identity.Spec.OpenStack.ServerGroupID != nil { - if err := computeService.DeleteServerGroup(ctx, *identity.Spec.OpenStack.ServerGroupID); err != nil { + if openstackIdentity.Spec.UserID != nil && openstackIdentity.Spec.ProjectID != nil { + // Rescope to the user/project... + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *openstackIdentity.Spec.UserID, *openstackIdentity.Spec.Password, *openstackIdentity.Spec.ProjectID) + + computeService, err := NewComputeClient(ctx, providerClient, p.region.Spec.Openstack.Compute) + if err != nil { return err } + + if openstackIdentity.Spec.ServerGroupID != nil { + if err := computeService.DeleteServerGroup(ctx, *openstackIdentity.Spec.ServerGroupID); err != nil { + return err + } + } } identityService, err := p.identity(ctx) @@ -621,15 +721,19 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident return err } - if err := identityService.DeleteUser(ctx, identity.Spec.OpenStack.UserID); err != nil { - return err + if openstackIdentity.Spec.UserID != nil { + if err := identityService.DeleteUser(ctx, *openstackIdentity.Spec.UserID); err != nil { + return err + } } - if err := identityService.DeleteProject(ctx, identity.Spec.OpenStack.ProjectID); err != nil { - return err + if openstackIdentity.Spec.ProjectID != nil { + if err := identityService.DeleteProject(ctx, *openstackIdentity.Spec.ProjectID); err != nil { + return err + } } - if err := p.client.Delete(ctx, identity); err != nil { + if err := p.client.Delete(ctx, openstackIdentity); err != nil { return err } @@ -638,24 +742,34 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident // CreatePhysicalNetwork creates a physical network for an identity. func (p *Provider) CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, request *openapi.PhysicalNetworkWrite) (*unikornv1.PhysicalNetwork, error) { + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) + if err != nil { + return nil, err + } + networkService, err := p.network(ctx) if err != nil { return nil, err } - vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "cluster-provider-network", identity.Spec.OpenStack.ProjectID) + vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "cluster-provider-network", *openstackIdentity.Spec.ProjectID) + if err != nil { + return nil, err + } + + userinfo, err := authorization.UserinfoFromContext(ctx) if err != nil { return nil, err } - objectMeta := conversion.NewObjectMetadata(&request.Metadata, p.region.Namespace) + objectMeta := conversion.NewObjectMetadata(&request.Metadata, p.region.Namespace, userinfo.Sub) objectMeta = objectMeta.WithOrganization(identity.Labels[coreconstants.OrganizationLabel]) objectMeta = objectMeta.WithProject(identity.Labels[coreconstants.ProjectLabel]) objectMeta = objectMeta.WithLabel(constants.RegionLabel, p.region.Name) objectMeta = objectMeta.WithLabel(constants.IdentityLabel, identity.Name) physicalNetwork := &unikornv1.PhysicalNetwork{ - ObjectMeta: objectMeta.Get(ctx), + ObjectMeta: objectMeta.Get(), Spec: unikornv1.PhysicalNetworkSpec{ Tags: convertTagList(request.Spec.Tags), ProviderNetwork: &unikornv1.OpenstackProviderNetworkSpec{ diff --git a/pkg/provisioners/managers/identity/provisioner.go b/pkg/provisioners/managers/identity/provisioner.go new file mode 100644 index 0000000..980134e --- /dev/null +++ b/pkg/provisioners/managers/identity/provisioner.go @@ -0,0 +1,89 @@ +/* +Copyright 2024 the Unikorn Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package identity + +import ( + "context" + + unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" + coreclient "github.com/unikorn-cloud/core/pkg/client" + coremanager "github.com/unikorn-cloud/core/pkg/manager" + "github.com/unikorn-cloud/core/pkg/provisioners" + unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" + "github.com/unikorn-cloud/region/pkg/constants" + "github.com/unikorn-cloud/region/pkg/handler/region" +) + +// Provisioner encapsulates control plane provisioning. +type Provisioner struct { + provisioners.Metadata + + // identity is the identity we're provisioning. + identity *unikornv1.Identity +} + +// New returns a new initialized provisioner object. +func New(_ coremanager.ControllerOptions) provisioners.ManagerProvisioner { + return &Provisioner{ + identity: &unikornv1.Identity{}, + } +} + +// Ensure the ManagerProvisioner interface is implemented. +var _ provisioners.ManagerProvisioner = &Provisioner{} + +func (p *Provisioner) Object() unikornv1core.ManagableResourceInterface { + return p.identity +} + +// Provision implements the Provision interface. +func (p *Provisioner) Provision(ctx context.Context) error { + cli, err := coreclient.ProvisionerClientFromContext(ctx) + if err != nil { + return err + } + + provider, err := region.NewClient(cli, p.identity.Namespace).Provider(ctx, p.identity.Labels[constants.RegionLabel]) + if err != nil { + return err + } + + if err := provider.CreateIdentity(ctx, p.identity); err != nil { + return err + } + + return nil +} + +// Deprovision implements the Provision interface. +func (p *Provisioner) Deprovision(ctx context.Context) error { + cli, err := coreclient.ProvisionerClientFromContext(ctx) + if err != nil { + return err + } + + provider, err := region.NewClient(cli, p.identity.Namespace).Provider(ctx, p.identity.Labels[constants.RegionLabel]) + if err != nil { + return err + } + + if err := provider.DeleteIdentity(ctx, p.identity); err != nil { + return err + } + + return nil +} diff --git a/pkg/reaper/reaper.go b/pkg/reaper/reaper.go deleted file mode 100644 index d8d20d6..0000000 --- a/pkg/reaper/reaper.go +++ /dev/null @@ -1,151 +0,0 @@ -/* -Copyright 2024 the Unikorn Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package reaper - -import ( - "context" - "errors" - "fmt" - - coreconstants "github.com/unikorn-cloud/core/pkg/constants" - unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" - "github.com/unikorn-cloud/region/pkg/constants" - "github.com/unikorn-cloud/region/pkg/handler/region" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/fields" - "k8s.io/apimachinery/pkg/watch" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -var ( - ErrInvalidClient = errors.New("client invalid") - - ErrDataMissing = errors.New("data missing") - - ErrUnexpectedType = errors.New("unexpected type") - - ErrCleanup = errors.New("cleanup failed") -) - -// Seasons don't fear the reaper, nor do wind or the sun or the rain. -// Peforms asynchronous cleanup tasks. -type Reaper struct { - client client.Client - namespace string -} - -func New(client client.Client, namespace string) *Reaper { - return &Reaper{ - client: client, - namespace: namespace, - } -} - -func (r *Reaper) Run(ctx context.Context) error { - log := log.FromContext(ctx) - - watchingClient, ok := r.client.(client.WithWatch) - if !ok { - return fmt.Errorf("%w: client does not implement watches", ErrInvalidClient) - } - - // TODO: This is insecure, enything can raise an event and have identities deleted. - // We should syncronously trigger the cleanup from various component deprovisioners - // but then you are faced with the problem that there is no access token to allow - // API access. - options := &client.ListOptions{ - FieldSelector: fields.SelectorFromSet(fields.Set{ - "reason": coreconstants.IdentityCleanupReadyEventReason, - }), - } - - // Please note when using Kubernetes watches of events that you may see - // some historical events for things in the last hour. - go func() { - var zeroEvent watch.Event - - for { - var events corev1.EventList - - watcher, err := watchingClient.Watch(ctx, &events, options) - if err != nil { - log.Error(err, "failed to setup watch") - continue - } - - eventStream := watcher.ResultChan() - - for { - event := <-eventStream - - // Zero value returned, closed channel, this happens, start it back - // up again... - if event == zeroEvent { - break - } - - log.V(1).Info("witnessed an event", "event", event) - - if event.Type != watch.Added { - continue - } - - realEvent, ok := event.Object.(*corev1.Event) - if !ok { - log.Error(ErrUnexpectedType, "unable to decode event") - } - - if err := r.handleEvent(ctx, realEvent); err != nil { - log.Error(err, "event handling failed") - } - } - } - }() - - return nil -} - -func (r *Reaper) handleEvent(ctx context.Context, event *corev1.Event) error { - log := log.FromContext(ctx) - - log.Info("processing cluster deletion event", "event", event) - - identityID, ok := event.Annotations[coreconstants.CloudIdentityAnnotation] - if !ok { - return fmt.Errorf("%w: identity annotation not present", ErrDataMissing) - } - - identity := &unikornv1.Identity{} - - if err := r.client.Get(ctx, client.ObjectKey{Namespace: r.namespace, Name: identityID}, identity); err != nil { - return err - } - - provider, err := region.NewClient(r.client, r.namespace).Provider(ctx, identity.Labels[constants.RegionLabel]) - if err != nil { - return fmt.Errorf("%w: failed to create provider client", err) - } - - if err := provider.DeleteIdentity(ctx, identity); err != nil { - return fmt.Errorf("%w: failed to delete identity", err) - } - - return nil -} diff --git a/pkg/server/options.go b/pkg/server/options.go index 2e745b7..16e58dd 100644 --- a/pkg/server/options.go +++ b/pkg/server/options.go @@ -47,10 +47,6 @@ type Options struct { // turn creates a project. WriteTimeout time.Duration - // OTLPEndpoint defines whether to ship spans to an OTLP consumer or - // not, and where to send them to. - OTLPEndpoint string - // RequestTimeout places a hard limit on all requests lengths. RequestTimeout time.Duration } @@ -63,5 +59,4 @@ func (o *Options) AddFlags(f *pflag.FlagSet) { f.DurationVar(&o.ReadHeaderTimeout, "server-read-header-timeout", time.Second, "How long to wait for the client to send headers.") f.DurationVar(&o.WriteTimeout, "server-write-timeout", 10*time.Second, "How long to wait for the API to respond to the client.") f.DurationVar(&o.RequestTimeout, "server-request-timeout", 30*time.Second, "How long to wait of a request to be serviced.") - f.StringVar(&o.OTLPEndpoint, "otlp-endpoint", "", "An optional OTLP endpoint to ship spans to.") } diff --git a/pkg/server/server.go b/pkg/server/server.go index 6d2fff5..3fbff0a 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -26,17 +26,16 @@ import ( chi "github.com/go-chi/chi/v5" "github.com/spf13/pflag" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/trace" + coreclient "github.com/unikorn-cloud/core/pkg/client" + "github.com/unikorn-cloud/core/pkg/manager/otel" coreapi "github.com/unikorn-cloud/core/pkg/openapi" - "github.com/unikorn-cloud/core/pkg/server/middleware/audit" "github.com/unikorn-cloud/core/pkg/server/middleware/cors" "github.com/unikorn-cloud/core/pkg/server/middleware/opentelemetry" "github.com/unikorn-cloud/core/pkg/server/middleware/timeout" identityclient "github.com/unikorn-cloud/identity/pkg/client" + "github.com/unikorn-cloud/identity/pkg/middleware/audit" openapimiddleware "github.com/unikorn-cloud/identity/pkg/middleware/openapi" openapimiddlewareremote "github.com/unikorn-cloud/identity/pkg/middleware/openapi/remote" "github.com/unikorn-cloud/region/pkg/constants" @@ -58,20 +57,32 @@ type Server struct { // HandlerOptions sets options for the HTTP handler. HandlerOptions handler.Options + // ClientOptions are for generic TLS client options e.g. certificates. + ClientOptions coreclient.HTTPClientOptions + // IdentityOptions allow configuration of the authorization middleware. - IdentityOptions identityclient.Options + IdentityOptions *identityclient.Options // CORSOptions are for remote resource sharing. CORSOptions cors.Options + + // OTelOptions are for tracing. + OTelOptions otel.Options } func (s *Server) AddFlags(goflags *flag.FlagSet, flags *pflag.FlagSet) { + if s.IdentityOptions == nil { + s.IdentityOptions = identityclient.NewOptions() + } + s.ZapOptions.BindFlags(goflags) s.Options.AddFlags(flags) s.HandlerOptions.AddFlags(flags) + s.ClientOptions.AddFlags(flags) s.IdentityOptions.AddFlags(flags) s.CORSOptions.AddFlags(flags) + s.OTelOptions.AddFlags(flags) } func (s *Server) SetupLogging() { @@ -82,30 +93,7 @@ func (s *Server) SetupLogging() { // logs by default, and optionally ship the spans to an OTLP listener. // TODO: move config into an otel specific options struct. func (s *Server) SetupOpenTelemetry(ctx context.Context) error { - otel.SetLogger(log.Log) - - otel.SetTextMapPropagator(propagation.TraceContext{}) - - opts := []trace.TracerProviderOption{ - trace.WithSpanProcessor(&opentelemetry.LoggingSpanProcessor{}), - } - - if s.Options.OTLPEndpoint != "" { - exporter, err := otlptracehttp.New(ctx, - otlptracehttp.WithEndpoint(s.Options.OTLPEndpoint), - otlptracehttp.WithInsecure(), - ) - - if err != nil { - return err - } - - opts = append(opts, trace.WithBatcher(exporter)) - } - - otel.SetTracerProvider(trace.NewTracerProvider(opts...)) - - return nil + return s.OTelOptions.Setup(ctx, trace.WithSpanProcessor(&opentelemetry.LoggingSpanProcessor{})) } func (s *Server) GetServer(client client.Client) (*http.Server, error) { @@ -143,7 +131,7 @@ func (s *Server) GetServer(client client.Client) (*http.Server, error) { router.NotFound(http.HandlerFunc(handler.NotFound)) router.MethodNotAllowed(http.HandlerFunc(handler.MethodNotAllowed)) - authorizer := openapimiddlewareremote.NewAuthorizer(client, s.Options.Namespace, &s.IdentityOptions) + authorizer := openapimiddlewareremote.NewAuthorizer(client, s.IdentityOptions, &s.ClientOptions) // Middleware specified here is applied to all requests post-routing. // NOTE: these are applied in reverse order!! @@ -156,7 +144,7 @@ func (s *Server) GetServer(client client.Client) (*http.Server, error) { }, } - identity := identityclient.New(client, s.Options.Namespace, &s.IdentityOptions) + identity := identityclient.New(client, s.IdentityOptions, &s.ClientOptions) handlerInterface, err := handler.New(client, s.Options.Namespace, &s.HandlerOptions, identity) if err != nil {