From 12ee1158ef12255a448b4f83ee9d0f37fdeb96be Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Wed, 7 Aug 2024 09:44:51 +0100 Subject: [PATCH] Rejig Neutron VLAN Provisioning So it transpires we were trying to piggy back on the stellar work by SCS for identity and allow a domain admin for provider networks, but alas Neutron has zero visibility of domains, and secondly only "admin" and "advsvc" can provision in a different project (hard coded, not a policy). Out one remaining option is to create a context that is for the "manager" user, but scoped to the user's project, and that can allow the provider network to be provisioned. --- Makefile | 3 +- charts/region/Chart.yaml | 6 +- ...n-cloud.org_openstackphysicalnetworks.yaml | 72 +++ ...on.unikorn-cloud.org_physicalnetworks.yaml | 87 +++- ...ion.unikorn-cloud.org_vlanallocations.yaml | 75 +++ charts/region/templates/_helpers.tpl | 4 + .../identity-controller/clusterrole.yaml | 9 + .../clusterrole.yaml | 64 +++ .../clusterrolebinding.yaml | 14 + .../deployment.yaml | 40 ++ .../physical-network-controller/role.yaml | 23 + .../rolebinding.yaml | 14 + .../serviceaccount.yaml | 10 + .../region-controller/clusterrole.yaml | 2 + .../templates/region-controller/ingress.yaml | 6 +- charts/region/values.yaml | 4 + .../main.go | 27 ++ .../.dockerignore | 2 + .../Dockerfile | 8 + go.mod | 6 +- go.sum | 8 +- pkg/apis/unikorn/v1alpha1/helpers.go | 24 + pkg/apis/unikorn/v1alpha1/register.go | 4 +- pkg/apis/unikorn/v1alpha1/types.go | 91 +++- .../unikorn/v1alpha1/zz_generated.deepcopy.go | 242 +++++++++- pkg/constants/constants.go | 3 + pkg/handler/handler.go | 145 +++++- pkg/handler/region/region.go | 6 +- pkg/managers/physicalnetwork/manager.go | 79 ++++ pkg/openapi/client.go | 374 +++++++++++++-- pkg/openapi/router.go | 148 +++++- pkg/openapi/schema.go | 186 ++++---- pkg/openapi/server.spec.yaml | 125 ++++- pkg/openapi/types.go | 63 ++- pkg/providers/allocation/vlan/vlan.go | 175 +++++++ pkg/providers/interfaces.go | 7 +- pkg/providers/openstack/network.go | 165 +++++-- pkg/providers/openstack/network_test.go | 71 --- pkg/providers/openstack/provider.go | 438 ++++++++++++++---- .../managers/identity/provisioner.go | 43 ++ .../managers/physicalnetwork/provisioner.go | 140 ++++++ 41 files changed, 2611 insertions(+), 402 deletions(-) create mode 100644 charts/region/crds/region.unikorn-cloud.org_openstackphysicalnetworks.yaml create mode 100644 charts/region/crds/region.unikorn-cloud.org_vlanallocations.yaml create mode 100644 charts/region/templates/physical-network-controller/clusterrole.yaml create mode 100644 charts/region/templates/physical-network-controller/clusterrolebinding.yaml create mode 100644 charts/region/templates/physical-network-controller/deployment.yaml create mode 100644 charts/region/templates/physical-network-controller/role.yaml create mode 100644 charts/region/templates/physical-network-controller/rolebinding.yaml create mode 100644 charts/region/templates/physical-network-controller/serviceaccount.yaml create mode 100644 cmd/unikorn-physical-network-controller/main.go create mode 100644 docker/unikorn-physical-network-controller/.dockerignore create mode 100644 docker/unikorn-physical-network-controller/Dockerfile create mode 100644 pkg/managers/physicalnetwork/manager.go create mode 100644 pkg/providers/allocation/vlan/vlan.go delete mode 100644 pkg/providers/openstack/network_test.go create mode 100644 pkg/provisioners/managers/physicalnetwork/provisioner.go diff --git a/Makefile b/Makefile index 071c40e..caa0b49 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,8 @@ REVISION := $(shell git rev-parse HEAD) # want to be amd64. CONTROLLERS = \ unikorn-region-controller \ - unikorn-identity-controller + unikorn-identity-controller \ + unikorn-physical-network-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 ae5053f..bb79d1a 100644 --- a/charts/region/Chart.yaml +++ b/charts/region/Chart.yaml @@ -4,12 +4,12 @@ description: A Helm chart for deploying Unikorn's Region Controller type: application -version: v0.1.33 -appVersion: v0.1.33 +version: v0.1.34 +appVersion: v0.1.34 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png dependencies: - name: unikorn-common - version: v0.1.6 + version: v0.1.8 repository: https://unikorn-cloud.github.io/helm-common diff --git a/charts/region/crds/region.unikorn-cloud.org_openstackphysicalnetworks.yaml b/charts/region/crds/region.unikorn-cloud.org_openstackphysicalnetworks.yaml new file mode 100644 index 0000000..c85bff9 --- /dev/null +++ b/charts/region/crds/region.unikorn-cloud.org_openstackphysicalnetworks.yaml @@ -0,0 +1,72 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: openstackphysicalnetworks.region.unikorn-cloud.org +spec: + group: region.unikorn-cloud.org + names: + categories: + - unikorn + kind: OpenstackPhysicalNetwork + listKind: OpenstackPhysicalNetworkList + plural: openstackphysicalnetworks + singular: openstackphysicalnetwork + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: OpenstackPhysicalNetwork defines a physical network beloning + to an identity. + 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: + networkID: + description: NetworkID is the network ID. + type: string + routerID: + description: RouterID is the router ID. + type: string + routerSubnetInterfaceAdded: + description: RouterSubnetInterfaceAdded tells us if this step has + been accomplished. + type: boolean + subnetID: + description: SubnetID is the subnet ID. + type: string + vlanID: + description: VlanID is the ID if the VLAN for IPAM. + type: integer + 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 4cc5f7f..2e8d396 100644 --- a/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml +++ b/charts/region/crds/region.unikorn-cloud.org_physicalnetworks.yaml @@ -47,21 +47,24 @@ spec: type: object spec: properties: - providerNetwork: - description: |- - ProviderNetwork is the provider network for port allocation of - virtual machines. - properties: - id: - description: ID is the network ID. - type: string - vlanID: - description: VlanID is the ID if the VLAN for IPAM. - type: integer - required: - - id - - vlanID - type: object + dnsNameservers: + description: DNSNameservers are a set of DNS nameservrs for the network. + items: + pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])$ + type: string + type: array + pause: + description: Pause, if true, will inhibit reconciliation. + type: boolean + prefix: + description: Prefix is the IPv4 address prefix. + pattern: ^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9]?[0-9])\/(?:3[0-2]|[1-2]?[0-9])$ + type: string + provider: + description: Provider defines the provider type. + enum: + - openstack + type: string tags: description: |- Tags are an abitrary list of key/value pairs that a client @@ -80,12 +83,64 @@ spec: - value type: object type: array + required: + - dnsNameservers + - prefix + - 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 type: object served: true storage: true - subresources: {} + subresources: + status: {} diff --git a/charts/region/crds/region.unikorn-cloud.org_vlanallocations.yaml b/charts/region/crds/region.unikorn-cloud.org_vlanallocations.yaml new file mode 100644 index 0000000..88b29b8 --- /dev/null +++ b/charts/region/crds/region.unikorn-cloud.org_vlanallocations.yaml @@ -0,0 +1,75 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: vlanallocations.region.unikorn-cloud.org +spec: + group: region.unikorn-cloud.org + names: + categories: + - unikorn + kind: VLANAllocation + listKind: VLANAllocationList + plural: vlanallocations + singular: vlanallocation + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: |- + VLANAllocation is used to manage VLAN allocations. Only a single instance is + allowed per region. As this is a custom resource, we are guaranteed atomicity + due to Kubernetes' speculative locking implementation. + 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: + allocations: + description: Allocations are an explcit set of VLAN allocations. + items: + properties: + id: + description: ID is the VLAN ID. + type: integer + physicalNetworkID: + description: |- + PhysicalNetworkID is the physical network/provider specific physical network + identifier that owns this entry. + type: string + required: + - id + - physicalNetworkID + type: object + type: array + type: object + status: + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/charts/region/templates/_helpers.tpl b/charts/region/templates/_helpers.tpl index 18bf26d..3a9e961 100644 --- a/charts/region/templates/_helpers.tpl +++ b/charts/region/templates/_helpers.tpl @@ -9,6 +9,10 @@ Create the container images {{- .Values.identityController.image | default (printf "%s/unikorn-identity-controller:%s" (include "unikorn.defaultRepositoryPath" .) (.Values.tag | default .Chart.Version)) }} {{- end }} +{{- define "unikorn.physicalNetworkControllerImage" -}} +{{- .Values.physicalNetworkController.image | default (printf "%s/unikorn-physical-network-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 index ab41885..4beb737 100644 --- a/charts/region/templates/identity-controller/clusterrole.yaml +++ b/charts/region/templates/identity-controller/clusterrole.yaml @@ -31,6 +31,15 @@ rules: - create - update - delete +# Cascading deletion. +- apiGroups: + - region.unikorn-cloud.org + resources: + - physicalnetworks + verbs: + - list + - watch + - delete - apiGroups: - "" resources: diff --git a/charts/region/templates/physical-network-controller/clusterrole.yaml b/charts/region/templates/physical-network-controller/clusterrole.yaml new file mode 100644 index 0000000..fa0d49d --- /dev/null +++ b/charts/region/templates/physical-network-controller/clusterrole.yaml @@ -0,0 +1,64 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: unikorn-physical-network-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +rules: +# Orchestrate Unikorn resources (my job). +- apiGroups: + - region.unikorn-cloud.org + resources: + - identities + - openstackidentities + verbs: + - list + - watch +- apiGroups: + - region.unikorn-cloud.org + resources: + - physicalnetworks + verbs: + - list + - watch + - update + - patch +- apiGroups: + - region.unikorn-cloud.org + resources: + - physicalnetworks/status + verbs: + - update +- apiGroups: + - region.unikorn-cloud.org + resources: + - openstackphysicalnetworks + verbs: + - list + - watch + - create + - update + - delete +- apiGroups: + - region.unikorn-cloud.org + resources: + - vlanallocations + verbs: + - list + - watch + - create + - update +- apiGroups: + - "" + resources: + - secrets + verbs: + - list + - watch +- apiGroups: + - region.unikorn-cloud.org + resources: + - regions + verbs: + - list + - watch diff --git a/charts/region/templates/physical-network-controller/clusterrolebinding.yaml b/charts/region/templates/physical-network-controller/clusterrolebinding.yaml new file mode 100644 index 0000000..0762b51 --- /dev/null +++ b/charts/region/templates/physical-network-controller/clusterrolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: unikorn-physical-network-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + namespace: {{ .Release.Namespace }} + name: unikorn-physical-network-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: unikorn-physical-network-controller diff --git a/charts/region/templates/physical-network-controller/deployment.yaml b/charts/region/templates/physical-network-controller/deployment.yaml new file mode 100644 index 0000000..747e64a --- /dev/null +++ b/charts/region/templates/physical-network-controller/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: unikorn-physical-network-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app: unikorn-physical-network-controller + template: + metadata: + labels: + app: unikorn-physical-network-controller + spec: + containers: + - name: unikorn-physical-network-controller + image: {{ include "unikorn.physicalNetworkControllerImage" . }} + 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-physical-network-controller + securityContext: + runAsNonRoot: true diff --git a/charts/region/templates/physical-network-controller/role.yaml b/charts/region/templates/physical-network-controller/role.yaml new file mode 100644 index 0000000..173cb53 --- /dev/null +++ b/charts/region/templates/physical-network-controller/role.yaml @@ -0,0 +1,23 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: unikorn-physical-network-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/physical-network-controller/rolebinding.yaml b/charts/region/templates/physical-network-controller/rolebinding.yaml new file mode 100644 index 0000000..d23e978 --- /dev/null +++ b/charts/region/templates/physical-network-controller/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: unikorn-physical-network-controller + labels: + {{- include "unikorn.labels" . | nindent 4 }} +subjects: +- kind: ServiceAccount + namespace: {{ .Release.Namespace }} + name: unikorn-physical-network-controller +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: unikorn-physical-network-controller diff --git a/charts/region/templates/physical-network-controller/serviceaccount.yaml b/charts/region/templates/physical-network-controller/serviceaccount.yaml new file mode 100644 index 0000000..7f6d280 --- /dev/null +++ b/charts/region/templates/physical-network-controller/serviceaccount.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: unikorn-physical-network-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 dbf86bc..7b22087 100644 --- a/charts/region/templates/region-controller/clusterrole.yaml +++ b/charts/region/templates/region-controller/clusterrole.yaml @@ -17,6 +17,7 @@ rules: - region.unikorn-cloud.org resources: - identities + - physicalnetworks verbs: - list - watch @@ -26,6 +27,7 @@ rules: - region.unikorn-cloud.org resources: - openstackidentities + - openstackphysicalnetworks verbs: - list - watch diff --git a/charts/region/templates/region-controller/ingress.yaml b/charts/region/templates/region-controller/ingress.yaml index 16418c6..336ef09 100644 --- a/charts/region/templates/region-controller/ingress.yaml +++ b/charts/region/templates/region-controller/ingress.yaml @@ -6,11 +6,7 @@ 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" + {{- include "unikorn.ingress.mtls.annotations" . | nindent 4 }} {{- 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 64c49d5..f31a74c 100644 --- a/charts/region/values.yaml +++ b/charts/region/values.yaml @@ -106,6 +106,10 @@ identityController: # Allow override of the identity controller image. image: +physicalNetworkController: + # 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-physical-network-controller/main.go b/cmd/unikorn-physical-network-controller/main.go new file mode 100644 index 0000000..d7a4959 --- /dev/null +++ b/cmd/unikorn-physical-network-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/physicalnetwork" +) + +func main() { + manager.Run(&physicalnetwork.Factory{}) +} diff --git a/docker/unikorn-physical-network-controller/.dockerignore b/docker/unikorn-physical-network-controller/.dockerignore new file mode 100644 index 0000000..e2d7070 --- /dev/null +++ b/docker/unikorn-physical-network-controller/.dockerignore @@ -0,0 +1,2 @@ +* +!bin/*-linux-gnu/unikorn-physical-network-controller diff --git a/docker/unikorn-physical-network-controller/Dockerfile b/docker/unikorn-physical-network-controller/Dockerfile new file mode 100644 index 0000000..92e3025 --- /dev/null +++ b/docker/unikorn-physical-network-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-physical-network-controller / + +ENTRYPOINT ["/unikorn-physical-network-controller"] diff --git a/go.mod b/go.mod index f789201..408e93a 100644 --- a/go.mod +++ b/go.mod @@ -9,9 +9,8 @@ require ( github.com/gophercloud/utils v0.0.0-20231010081019-80377eca5d56 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.66 - github.com/unikorn-cloud/identity v0.2.30 + github.com/unikorn-cloud/core v0.1.68 + github.com/unikorn-cloud/identity v0.2.32 go.opentelemetry.io/otel v1.28.0 go.opentelemetry.io/otel/sdk v1.28.0 go.opentelemetry.io/otel/trace v1.28.0 @@ -61,7 +60,6 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 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 diff --git a/go.sum b/go.sum index ed75d2c..8854ca6 100644 --- a/go.sum +++ b/go.sum @@ -135,10 +135,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT 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.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/unikorn-cloud/core v0.1.68 h1:uYbqC/A6ppnJiiKeRUHNcRazThOX4W97zjHAyP9QWqY= +github.com/unikorn-cloud/core v0.1.68/go.mod h1:MhA9xeueMB7EJgdyF0rE7lFQSkuHEcoPen6iwt/QCBE= +github.com/unikorn-cloud/identity v0.2.32 h1:Kx0SSJQgMwSXKatvSDsRu7JAuXnT9Cs5tQg99GcdfH4= +github.com/unikorn-cloud/identity v0.2.32/go.mod h1:g3ms1lZDvsGECk6EXYkdvJDuSIfoMvAV03GR9qeIvaM= 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= diff --git a/pkg/apis/unikorn/v1alpha1/helpers.go b/pkg/apis/unikorn/v1alpha1/helpers.go index 041e222..f0b528b 100644 --- a/pkg/apis/unikorn/v1alpha1/helpers.go +++ b/pkg/apis/unikorn/v1alpha1/helpers.go @@ -46,3 +46,27 @@ func (c *Identity) StatusConditionWrite(t unikornv1core.ConditionType, status co func (c *Identity) ResourceLabels() (labels.Set, error) { return nil, nil } + +// Paused implements the ReconcilePauser interface. +func (c *PhysicalNetwork) Paused() bool { + return c.Spec.Pause +} + +// StatusConditionRead scans the status conditions for an existing condition whose type +// matches. +func (c *PhysicalNetwork) 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 *PhysicalNetwork) 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 *PhysicalNetwork) ResourceLabels() (labels.Set, error) { + return nil, nil +} diff --git a/pkg/apis/unikorn/v1alpha1/register.go b/pkg/apis/unikorn/v1alpha1/register.go index fde2a74..7995729 100644 --- a/pkg/apis/unikorn/v1alpha1/register.go +++ b/pkg/apis/unikorn/v1alpha1/register.go @@ -50,8 +50,10 @@ var ( func init() { SchemeBuilder.Register(&Region{}, &RegionList{}) SchemeBuilder.Register(&Identity{}, &IdentityList{}) - SchemeBuilder.Register(&OpenstackIdentity{}, &OpenstackIdentityList{}) SchemeBuilder.Register(&PhysicalNetwork{}, &PhysicalNetworkList{}) + SchemeBuilder.Register(&OpenstackIdentity{}, &OpenstackIdentityList{}) + SchemeBuilder.Register(&OpenstackPhysicalNetwork{}, &OpenstackPhysicalNetworkList{}) + SchemeBuilder.Register(&VLANAllocation{}, &VLANAllocationList{}) } // Resource maps a resource type to a group resource. diff --git a/pkg/apis/unikorn/v1alpha1/types.go b/pkg/apis/unikorn/v1alpha1/types.go index 29d82d9..d4001b5 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -360,6 +360,7 @@ type PhysicalNetworkList struct { // +genclient // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object // +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:subresource:status // +kubebuilder:printcolumn:name="status",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].reason" // +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" type PhysicalNetwork struct { @@ -370,20 +371,94 @@ type PhysicalNetwork struct { } type PhysicalNetworkSpec 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"` - // ProviderNetwork is the provider network for port allocation of - // virtual machines. - ProviderNetwork *OpenstackProviderNetworkSpec `json:"providerNetwork,omitempty"` + // Provider defines the provider type. + Provider Provider `json:"provider"` + // Prefix is the IPv4 address prefix. + Prefix *unikornv1core.IPv4Prefix `json:"prefix"` + // DNSNameservers are a set of DNS nameservrs for the network. + DNSNameservers []unikornv1core.IPv4Address `json:"dnsNameservers"` } -type OpenstackProviderNetworkSpec struct { - // ID is the network ID. - ID string `json:"id"` +type PhysicalNetworkStatus struct { + // Current service state of a cluster manager. + Conditions []unikornv1core.Condition `json:"conditions,omitempty"` +} + +// OpenstackPhysicalNetworkList s a typed list of physical networks. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type OpenstackPhysicalNetworkList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenstackPhysicalNetwork `json:"items"` +} + +// OpenstackPhysicalNetwork defines a physical network beloning to an identity. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +type OpenstackPhysicalNetwork struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec OpenstackPhysicalNetworkSpec `json:"spec"` + Status OpenstackPhysicalNetworkStatus `json:"status,omitempty"` +} + +type OpenstackPhysicalNetworkSpec struct { + // NetworkID is the network ID. + NetworkID *string `json:"networkID,omitempty"` // VlanID is the ID if the VLAN for IPAM. - VlanID int `json:"vlanID"` + VlanID *int `json:"vlanID,omitempty"` + // SubnetID is the subnet ID. + SubnetID *string `json:"subnetID,omitempty"` + // RouterID is the router ID. + RouterID *string `json:"routerID,omitempty"` + // RouterSubnetInterfaceAdded tells us if this step has been accomplished. + RouterSubnetInterfaceAdded bool `json:"routerSubnetInterfaceAdded,omitempty"` } -type PhysicalNetworkStatus struct { +type OpenstackPhysicalNetworkStatus struct { +} + +// VLANAllocationList is a typed list of VLAN allocations. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type VLANAllocationList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []VLANAllocation `json:"items"` +} + +// VLANAllocation is used to manage VLAN allocations. Only a single instance is +// allowed per region. As this is a custom resource, we are guaranteed atomicity +// due to Kubernetes' speculative locking implementation. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +type VLANAllocation struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec VLANAllocationSpec `json:"spec"` + Status VLANAllocationStatus `json:"status,omitempty"` +} + +type VLANAllocationSpec struct { + // Allocations are an explcit set of VLAN allocations. + Allocations []VLANAllocationEntry `json:"allocations,omitempty"` +} + +type VLANAllocationEntry struct { + // ID is the VLAN ID. + ID int `json:"id"` + // PhysicalNetworkID is the physical network/provider specific physical network + // identifier that owns this entry. + PhysicalNetworkID string `json:"physicalNetworkID"` +} + +type VLANAllocationStatus struct { } diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index 4e23872..c09e137 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -481,17 +481,114 @@ func (in *OpenstackIdentityStatus) DeepCopy() *OpenstackIdentityStatus { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *OpenstackProviderNetworkSpec) DeepCopyInto(out *OpenstackProviderNetworkSpec) { +func (in *OpenstackPhysicalNetwork) DeepCopyInto(out *OpenstackPhysicalNetwork) { + *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 OpenstackPhysicalNetwork. +func (in *OpenstackPhysicalNetwork) DeepCopy() *OpenstackPhysicalNetwork { + if in == nil { + return nil + } + out := new(OpenstackPhysicalNetwork) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackPhysicalNetwork) 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 *OpenstackPhysicalNetworkList) DeepCopyInto(out *OpenstackPhysicalNetworkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenstackPhysicalNetwork, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackPhysicalNetworkList. +func (in *OpenstackPhysicalNetworkList) DeepCopy() *OpenstackPhysicalNetworkList { + if in == nil { + return nil + } + out := new(OpenstackPhysicalNetworkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackPhysicalNetworkList) 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 *OpenstackPhysicalNetworkSpec) DeepCopyInto(out *OpenstackPhysicalNetworkSpec) { + *out = *in + if in.NetworkID != nil { + in, out := &in.NetworkID, &out.NetworkID + *out = new(string) + **out = **in + } + if in.VlanID != nil { + in, out := &in.VlanID, &out.VlanID + *out = new(int) + **out = **in + } + if in.SubnetID != nil { + in, out := &in.SubnetID, &out.SubnetID + *out = new(string) + **out = **in + } + if in.RouterID != nil { + in, out := &in.RouterID, &out.RouterID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackPhysicalNetworkSpec. +func (in *OpenstackPhysicalNetworkSpec) DeepCopy() *OpenstackPhysicalNetworkSpec { + if in == nil { + return nil + } + out := new(OpenstackPhysicalNetworkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackPhysicalNetworkStatus) DeepCopyInto(out *OpenstackPhysicalNetworkStatus) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackProviderNetworkSpec. -func (in *OpenstackProviderNetworkSpec) DeepCopy() *OpenstackProviderNetworkSpec { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackPhysicalNetworkStatus. +func (in *OpenstackPhysicalNetworkStatus) DeepCopy() *OpenstackPhysicalNetworkStatus { if in == nil { return nil } - out := new(OpenstackProviderNetworkSpec) + out := new(OpenstackPhysicalNetworkStatus) in.DeepCopyInto(out) return out } @@ -502,7 +599,7 @@ func (in *PhysicalNetwork) DeepCopyInto(out *PhysicalNetwork) { out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status + in.Status.DeepCopyInto(&out.Status) return } @@ -565,10 +662,16 @@ func (in *PhysicalNetworkSpec) DeepCopyInto(out *PhysicalNetworkSpec) { *out = make(TagList, len(*in)) copy(*out, *in) } - if in.ProviderNetwork != nil { - in, out := &in.ProviderNetwork, &out.ProviderNetwork - *out = new(OpenstackProviderNetworkSpec) - **out = **in + if in.Prefix != nil { + in, out := &in.Prefix, &out.Prefix + *out = (*in).DeepCopy() + } + if in.DNSNameservers != nil { + in, out := &in.DNSNameservers, &out.DNSNameservers + *out = make([]unikornv1alpha1.IPv4Address, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } return } @@ -586,6 +689,13 @@ func (in *PhysicalNetworkSpec) DeepCopy() *PhysicalNetworkSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PhysicalNetworkStatus) DeepCopyInto(out *PhysicalNetworkStatus) { *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 } @@ -901,6 +1011,120 @@ func (in TagList) DeepCopy() TagList { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VLANAllocation) DeepCopyInto(out *VLANAllocation) { + *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 VLANAllocation. +func (in *VLANAllocation) DeepCopy() *VLANAllocation { + if in == nil { + return nil + } + out := new(VLANAllocation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VLANAllocation) 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 *VLANAllocationEntry) DeepCopyInto(out *VLANAllocationEntry) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLANAllocationEntry. +func (in *VLANAllocationEntry) DeepCopy() *VLANAllocationEntry { + if in == nil { + return nil + } + out := new(VLANAllocationEntry) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VLANAllocationList) DeepCopyInto(out *VLANAllocationList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]VLANAllocation, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLANAllocationList. +func (in *VLANAllocationList) DeepCopy() *VLANAllocationList { + if in == nil { + return nil + } + out := new(VLANAllocationList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *VLANAllocationList) 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 *VLANAllocationSpec) DeepCopyInto(out *VLANAllocationSpec) { + *out = *in + if in.Allocations != nil { + in, out := &in.Allocations, &out.Allocations + *out = make([]VLANAllocationEntry, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLANAllocationSpec. +func (in *VLANAllocationSpec) DeepCopy() *VLANAllocationSpec { + if in == nil { + return nil + } + out := new(VLANAllocationSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *VLANAllocationStatus) DeepCopyInto(out *VLANAllocationStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new VLANAllocationStatus. +func (in *VLANAllocationStatus) DeepCopy() *VLANAllocationStatus { + if in == nil { + return nil + } + out := new(VLANAllocationStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *VLANSegment) DeepCopyInto(out *VLANSegment) { *out = *in diff --git a/pkg/constants/constants.go b/pkg/constants/constants.go index 66b9b9c..5101d74 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -51,4 +51,7 @@ const ( // IdentityLabel creates an indexable linkage between resources and an // owning identity. IdentityLabel = "regions.unikorn-cloud.org/identity-id" + // PhysicalNetworkLabel creates an indexable linkage between resources + // and an owning entity. + PhysicalNetworkLabel = "regions.unikorn-cloud.org/phyiscal-network-id" ) diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 651bc42..01cd676 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -23,6 +23,7 @@ import ( "context" "encoding/base64" "fmt" + "net" "net/http" "slices" "time" @@ -85,9 +86,9 @@ func (h *Handler) setUncacheable(w http.ResponseWriter) { } func (h *Handler) getIdentity(ctx context.Context, id string) (*unikornv1.Identity, error) { - identity := &unikornv1.Identity{} + resource := &unikornv1.Identity{} - if err := h.client.Get(ctx, client.ObjectKey{Namespace: h.namespace, Name: id}, identity); err != nil { + if err := h.client.Get(ctx, client.ObjectKey{Namespace: h.namespace, Name: id}, resource); err != nil { if kerrors.IsNotFound(err) { return nil, errors.HTTPNotFound().WithError(err) } @@ -95,7 +96,21 @@ func (h *Handler) getIdentity(ctx context.Context, id string) (*unikornv1.Identi return nil, errors.OAuth2ServerError("unable to lookup identity").WithError(err) } - return identity, nil + return resource, nil +} + +func (h *Handler) getPhysicalNetwork(ctx context.Context, id string) (*unikornv1.PhysicalNetwork, error) { + resource := &unikornv1.PhysicalNetwork{} + + if err := h.client.Get(ctx, client.ObjectKey{Namespace: h.namespace, Name: id}, resource); err != nil { + if kerrors.IsNotFound(err) { + return nil, errors.HTTPNotFound().WithError(err) + } + + return nil, errors.OAuth2ServerError("unable to physical network identity").WithError(err) + } + + return resource, nil } func (h *Handler) GetApiV1OrganizationsOrganizationIDRegions(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter) { @@ -487,20 +502,56 @@ func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentit w.WriteHeader(http.StatusAccepted) } -func convertPhysicalNetwork(in *unikornv1.PhysicalNetwork) *openapi.PhysicalNetworkRead { +func convertIPv4List(in []unikornv1core.IPv4Address) openapi.Ipv4AddressList { + out := make(openapi.Ipv4AddressList, len(in)) + + for i, ip := range in { + out[i] = ip.String() + } + + return out +} + +func (h *Handler) convertPhysicalNetwork(ctx context.Context, in *unikornv1.PhysicalNetwork) *openapi.PhysicalNetworkRead { + provisioningStatus := coreapi.ResourceProvisioningStatusUnknown + + if condition, err := in.StatusConditionRead(unikornv1core.ConditionAvailable); err == nil { + provisioningStatus = conversion.ConvertStatusCondition(condition) + } + out := &openapi.PhysicalNetworkRead{ - Metadata: conversion.ProjectScopedResourceReadMetadata(in, coreapi.ResourceProvisioningStatusProvisioned), + Metadata: conversion.ProjectScopedResourceReadMetadata(in, provisioningStatus), + Spec: &openapi.PhysicalNetworkReadSpec{ + RegionId: in.Labels[constants.RegionLabel], + Prefix: in.Spec.Prefix.String(), + DnsNameservers: convertIPv4List(in.Spec.DNSNameservers), + }, } if tags := convertTags(in.Spec.Tags); tags != nil { out.Spec.Tags = &tags } + switch in.Spec.Provider { + case unikornv1.ProviderOpenstack: + out.Spec.Type = openapi.Openstack + + var openstackPhysicalNetwork unikornv1.OpenstackPhysicalNetwork + + if err := h.client.Get(ctx, client.ObjectKey{Namespace: in.Namespace, Name: in.Name}, &openstackPhysicalNetwork); err == nil { + out.Spec.Openstack = &openapi.PhysicalNetworkSpecOpenstack{ + VlanId: openstackPhysicalNetwork.Spec.VlanID, + NetworkId: openstackPhysicalNetwork.Spec.NetworkID, + SubnetId: openstackPhysicalNetwork.Spec.SubnetID, + } + } + } + return out } -func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { - if err := rbac.AllowProjectScope(r.Context(), "identities", identityapi.Create, organizationID, projectID); err != nil { +func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { + if err := rbac.AllowProjectScope(r.Context(), "physicalnetworks", identityapi.Create, organizationID, projectID); err != nil { errors.HandleError(w, r, err) return } @@ -518,19 +569,93 @@ func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitie return } - provider, err := region.NewClient(h.client, h.namespace).Provider(r.Context(), identity.Labels[constants.RegionLabel]) + userinfo, err := authorization.UserinfoFromContext(r.Context()) + if err != nil { + errors.HandleError(w, r, errors.OAuth2ServerError("unable to get userinfo").WithError(err)) + return + } + + _, prefix, err := net.ParseCIDR(request.Spec.Prefix) if err != nil { + errors.HandleError(w, r, errors.OAuth2InvalidRequest("unable to parse prefix").WithError(err)) + return + } + + dnsNameservers := make([]unikornv1core.IPv4Address, len(request.Spec.DnsNameservers)) + + for i, ip := range request.Spec.DnsNameservers { + temp := net.ParseIP(ip) + if temp == nil { + errors.HandleError(w, r, errors.OAuth2InvalidRequest("unable to parse dns nameserver")) + return + } + + dnsNameservers[i] = unikornv1core.IPv4Address{ + IP: temp, + } + } + + network := &unikornv1.PhysicalNetwork{ + ObjectMeta: conversion.NewObjectMetadata(&request.Metadata, h.namespace, userinfo.Sub).WithOrganization(organizationID).WithProject(projectID).WithLabel(constants.RegionLabel, identity.Labels[constants.RegionLabel]).WithLabel(constants.IdentityLabel, identityID).Get(), + Spec: unikornv1.PhysicalNetworkSpec{ + Provider: identity.Spec.Provider, + Prefix: &unikornv1core.IPv4Prefix{ + IPNet: *prefix, + }, + DNSNameservers: dnsNameservers, + }, + } + + if request.Spec != nil { + network.Spec.Tags = generateTagList(request.Spec.Tags) + } + + if err := h.client.Create(r.Context(), network); err != nil { + errors.HandleError(w, r, errors.OAuth2ServerError("unable to create physical network").WithError(err)) + return + } + + util.WriteJSONResponse(w, r, http.StatusCreated, h.convertPhysicalNetwork(r.Context(), network)) +} + +func (h *Handler) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter, physicalNetworkID openapi.PhysicalNetworkIDParameter) { + if err := rbac.AllowProjectScope(r.Context(), "physicalnetworks", identityapi.Read, organizationID, projectID); err != nil { errors.HandleError(w, r, err) return } - network, err := provider.CreatePhysicalNetwork(r.Context(), identity, request) + resource, err := h.getPhysicalNetwork(r.Context(), physicalNetworkID) if err != nil { errors.HandleError(w, r, err) return } - util.WriteJSONResponse(w, r, http.StatusCreated, convertPhysicalNetwork(network)) + util.WriteJSONResponse(w, r, http.StatusOK, h.convertPhysicalNetwork(r.Context(), resource)) +} + +func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter, physicalNetworkID openapi.PhysicalNetworkIDParameter) { + if err := rbac.AllowProjectScope(r.Context(), "physicalnetworks", identityapi.Delete, organizationID, projectID); err != nil { + errors.HandleError(w, r, err) + return + } + + resource, err := h.getPhysicalNetwork(r.Context(), physicalNetworkID) + if err != nil { + errors.HandleError(w, r, err) + return + } + + if err := h.client.Delete(r.Context(), resource); err != nil { + if kerrors.IsNotFound(err) { + errors.HandleError(w, r, errors.HTTPNotFound().WithError(err)) + return + } + + errors.HandleError(w, r, errors.OAuth2ServerError("unable to delete physical network").WithError(err)) + return + } + + w.WriteHeader(http.StatusAccepted) } func convertExternalNetwork(in providers.ExternalNetwork) openapi.ExternalNetwork { diff --git a/pkg/handler/region/region.go b/pkg/handler/region/region.go index 35a654e..6489c6c 100644 --- a/pkg/handler/region/region.go +++ b/pkg/handler/region/region.go @@ -76,11 +76,11 @@ func findRegion(regions *unikornv1.RegionList, regionID string) (*unikornv1.Regi //nolint:gochecknoglobals var cache = map[string]providers.Provider{} -func (c Client) newProvider(region *unikornv1.Region) (providers.Provider, error) { +func (c Client) newProvider(ctx context.Context, region *unikornv1.Region) (providers.Provider, error) { //nolint:gocritic switch region.Spec.Provider { case unikornv1.ProviderOpenstack: - return openstack.New(c.client, region), nil + return openstack.New(ctx, c.client, region) } return nil, ErrRegionProviderUnimplmented @@ -101,7 +101,7 @@ func (c *Client) Provider(ctx context.Context, regionID string) (providers.Provi return provider, nil } - provider, err := c.newProvider(region) + provider, err := c.newProvider(ctx, region) if err != nil { return nil, err } diff --git a/pkg/managers/physicalnetwork/manager.go b/pkg/managers/physicalnetwork/manager.go new file mode 100644 index 0000000..89a3b32 --- /dev/null +++ b/pkg/managers/physicalnetwork/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 physicalnetwork + +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/physicalnetwork" + + "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, physicalnetwork.New) +} + +// RegisterWatches adds any watches that would trigger a reconcile. +func (*Factory) RegisterWatches(manager manager.Manager, controller controller.Controller) error { + // Any changes to the physicalnetwork spec, trigger a reconcile. + if err := controller.Watch(source.Kind(manager.GetCache(), &unikornv1.PhysicalNetwork{}, &handler.TypedEnqueueRequestForObject[*unikornv1.PhysicalNetwork]{}, &predicate.TypedGenerationChangedPredicate[*unikornv1.PhysicalNetwork]{})); 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 d0c6baa..50a83c3 100644 --- a/pkg/openapi/client.go +++ b/pkg/openapi/client.go @@ -104,10 +104,16 @@ type ClientInterface interface { // 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) + // 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) - PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + + // DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID request + DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) + + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID request + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) // GetApiV1OrganizationsOrganizationIDRegions request GetApiV1OrganizationsOrganizationIDRegions(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -182,8 +188,20 @@ func (c *Client) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesI 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) +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 { + 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) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksRequest(c.Server, organizationID, projectID, identityID, body) if err != nil { return nil, err } @@ -194,8 +212,20 @@ func (c *Client) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentities return c.Client.Do(req) } -func (c *Client) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { - req, err := NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequest(c.Server, organizationID, projectID, identityID, body) +func (c *Client) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest(c.Server, organizationID, projectID, identityID, physicalNetworkID) + 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) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest(c.Server, organizationID, projectID, identityID, physicalNetworkID) if err != nil { return nil, err } @@ -438,19 +468,19 @@ func NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID 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) { +// 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 buf, err := json.Marshal(body) if err != nil { return nil, err } bodyReader = bytes.NewReader(buf) - return NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(server, organizationID, projectID, identityID, "application/json", bodyReader) + return NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksRequestWithBody(server, organizationID, projectID, identityID, "application/json", bodyReader) } -// NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody generates requests for PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks with any type of body -func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader) (*http.Request, error) { +// NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksRequestWithBody generates requests for PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks with any type of body +func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksRequestWithBody(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader) (*http.Request, error) { var err error var pathParam0 string @@ -479,7 +509,7 @@ func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityI return nil, err } - operationPath := fmt.Sprintf("/api/v1/organizations/%s/projects/%s/identities/%s/physicalNetworks", pathParam0, pathParam1, pathParam2) + operationPath := fmt.Sprintf("/api/v1/organizations/%s/projects/%s/identities/%s/physicalnetworks", pathParam0, pathParam1, pathParam2) if operationPath[0] == '/' { operationPath = "." + operationPath } @@ -499,6 +529,116 @@ func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityI return req, nil } +// NewDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest generates requests for DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID +func NewDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) (*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 + } + + var pathParam3 string + + pathParam3, err = runtime.StyleParamWithLocation("simple", false, "physicalNetworkID", runtime.ParamLocationPath, physicalNetworkID) + 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/physicalnetworks/%s", pathParam0, pathParam1, pathParam2, pathParam3) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("DELETE", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + +// NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest generates requests for GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID +func NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) (*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 + } + + var pathParam3 string + + pathParam3, err = runtime.StyleParamWithLocation("simple", false, "physicalNetworkID", runtime.ParamLocationPath, physicalNetworkID) + 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/physicalnetworks/%s", pathParam0, pathParam1, pathParam2, pathParam3) + 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 +} + // NewGetApiV1OrganizationsOrganizationIDRegionsRequest generates requests for GetApiV1OrganizationsOrganizationIDRegions func NewGetApiV1OrganizationsOrganizationIDRegionsRequest(server string, organizationID OrganizationIDParameter) (*http.Request, error) { var err error @@ -713,10 +853,16 @@ type ClientWithResponsesInterface interface { // 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) + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksWithBodyWithResponse request with any body + PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksWithBodyWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse, error) + + PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse, error) - PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) + // DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse request + DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) + + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse request + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) // GetApiV1OrganizationsOrganizationIDRegionsWithResponse request GetApiV1OrganizationsOrganizationIDRegionsWithResponse(ctx context.Context, organizationID OrganizationIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDRegionsResponse, error) @@ -835,7 +981,7 @@ func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID return 0 } -type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse struct { +type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse struct { Body []byte HTTPResponse *http.Response JSON201 *PhysicalNetworkResponse @@ -846,7 +992,7 @@ type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPh } // Status returns HTTPResponse.Status -func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse) Status() string { +func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse) Status() string { if r.HTTPResponse != nil { return r.HTTPResponse.Status } @@ -854,7 +1000,58 @@ func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityI } // StatusCode returns HTTPResponse.StatusCode -func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse) StatusCode() int { +func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse struct { + Body []byte + HTTPResponse *http.Response + JSON400 *externalRef0.BadRequestResponse + JSON401 *externalRef0.UnauthorizedResponse + JSON403 *externalRef0.ForbiddenResponse + JSON500 *externalRef0.InternalServerErrorResponse +} + +// Status returns HTTPResponse.Status +func (r DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +type GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *PhysicalNetworkResponse + JSON400 *externalRef0.BadRequestResponse + JSON401 *externalRef0.UnauthorizedResponse + JSON403 *externalRef0.ForbiddenResponse + JSON500 *externalRef0.InternalServerErrorResponse +} + +// Status returns HTTPResponse.Status +func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse) StatusCode() int { if r.HTTPResponse != nil { return r.HTTPResponse.StatusCode } @@ -1005,21 +1202,39 @@ func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDProjectsProject 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...) +// 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...) + if err != nil { + return nil, err + } + return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse(rsp) +} + +func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse, error) { + rsp, err := c.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(ctx, organizationID, projectID, identityID, body, reqEditors...) if err != nil { return nil, err } - return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp) + return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse(rsp) } -func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { - rsp, err := c.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(ctx, organizationID, projectID, identityID, body, reqEditors...) +// DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse request returning *DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse +func (c *ClientWithResponses) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) { + rsp, err := c.DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx, organizationID, projectID, identityID, physicalNetworkID, reqEditors...) if err != nil { return nil, err } - return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp) + return ParseDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse(rsp) +} + +// GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse request returning *GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse +func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) { + rsp, err := c.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(ctx, organizationID, projectID, identityID, physicalNetworkID, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse(rsp) } // GetApiV1OrganizationsOrganizationIDRegionsWithResponse request returning *GetApiV1OrganizationsOrganizationIDRegionsResponse @@ -1274,15 +1489,15 @@ func ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentity return response, nil } -// ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse parses an HTTP response from a PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksWithResponse call -func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp *http.Response) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { +// ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse parses an HTTP response from a PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksWithResponse call +func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse(rsp *http.Response) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) defer func() { _ = rsp.Body.Close() }() if err != nil { return nil, err } - response := &PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksResponse{ + response := &PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksResponse{ Body: bodyBytes, HTTPResponse: rsp, } @@ -1328,6 +1543,107 @@ func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentit return response, nil } +// ParseDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse parses an HTTP response from a DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse call +func ParseDeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse(rsp *http.Response) (*DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + 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 +} + +// ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDWithResponse call +func ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkIDResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest PhysicalNetworkResponse + 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 +} + // ParseGetApiV1OrganizationsOrganizationIDRegionsResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDRegionsWithResponse call func ParseGetApiV1OrganizationsOrganizationIDRegionsResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDRegionsResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/openapi/router.go b/pkg/openapi/router.go index 1a03c51..55c7081 100644 --- a/pkg/openapi/router.go +++ b/pkg/openapi/router.go @@ -27,8 +27,14 @@ type ServerInterface interface { // (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) + // (POST /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks) + PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter) + + // (DELETE /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}) + DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) + + // (GET /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}) + GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) // (GET /api/v1/organizations/{organizationID}/regions) GetApiV1OrganizationsOrganizationIDRegions(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter) @@ -67,8 +73,18 @@ func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdent 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) { +// (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) +} + +// (DELETE /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}) +func (_ Unimplemented) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) { + w.WriteHeader(http.StatusNotImplemented) +} + +// (GET /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}) +func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, identityID IdentityIDParameter, physicalNetworkID PhysicalNetworkIDParameter) { w.WriteHeader(http.StatusNotImplemented) } @@ -258,8 +274,109 @@ func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDProjectsPr handler.ServeHTTP(w, r.WithContext(ctx)) } -// PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks operation middleware -func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request) { +// PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks operation middleware +func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(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.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks(w, r, organizationID, projectID, identityID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID operation middleware +func (siw *ServerInterfaceWrapper) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(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 + } + + // ------------- Path parameter "physicalNetworkID" ------------- + var physicalNetworkID PhysicalNetworkIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "physicalNetworkID", runtime.ParamLocationPath, chi.URLParam(r, "physicalNetworkID"), &physicalNetworkID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "physicalNetworkID", Err: err}) + return + } + + ctx = context.WithValue(ctx, Oauth2AuthenticationScopes, []string{}) + + handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + siw.Handler.DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w, r, organizationID, projectID, identityID, physicalNetworkID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + +// GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID operation middleware +func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w http.ResponseWriter, r *http.Request) { ctx := r.Context() var err error @@ -291,10 +408,19 @@ func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsP return } + // ------------- Path parameter "physicalNetworkID" ------------- + var physicalNetworkID PhysicalNetworkIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "physicalNetworkID", runtime.ParamLocationPath, chi.URLParam(r, "physicalNetworkID"), &physicalNetworkID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "physicalNetworkID", Err: err}) + return + } + ctx = context.WithValue(ctx, Oauth2AuthenticationScopes, []string{}) handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - siw.Handler.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks(w, r, organizationID, projectID, identityID) + siw.Handler.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID(w, r, organizationID, projectID, identityID, physicalNetworkID) })) for _, middleware := range siw.HandlerMiddlewares { @@ -569,7 +695,13 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl 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) + r.Post(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks", wrapper.PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks) + }) + r.Group(func(r chi.Router) { + r.Delete(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}", wrapper.DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID) + }) + r.Group(func(r chi.Router) { + r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}", wrapper.GetApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksPhysicalNetworkID) }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/regions", wrapper.GetApiV1OrganizationsOrganizationIDRegions) diff --git a/pkg/openapi/schema.go b/pkg/openapi/schema.go index 1eb12f9..004619f 100644 --- a/pkg/openapi/schema.go +++ b/pkg/openapi/schema.go @@ -19,96 +19,102 @@ 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 5fab2d1..6da5c4f 100644 --- a/pkg/openapi/server.spec.yaml +++ b/pkg/openapi/server.spec.yaml @@ -177,7 +177,7 @@ paths: $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/notFoundResponse' '500': $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/internalServerErrorResponse' - /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalNetworks: + /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks: description: |- Manages physical networks. Physical networks are networks that may be required for baremetal node provisioning e.g. a VLAN. Note that only a single provider network is currently @@ -187,7 +187,7 @@ paths: - $ref: '#/components/parameters/projectIDParameter' - $ref: '#/components/parameters/identityIDParameter' post: - description: Create a new provider network. + description: Create a new physical network. security: - oauth2Authentication: [] requestBody: @@ -203,6 +203,46 @@ paths: $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' + /api/v1/organizations/{organizationID}/projects/{projectID}/identities/{identityID}/physicalnetworks/{physicalNetworkID}: + description: |- + Manages physical networks. Physical networks are networks that may be required for + baremetal node provisioning e.g. a VLAN. Note that only a single provider network is currently + supported per identity, as identities are intended to exist per piece of infrastructure. + parameters: + - $ref: '#/components/parameters/organizationIDParameter' + - $ref: '#/components/parameters/projectIDParameter' + - $ref: '#/components/parameters/identityIDParameter' + - $ref: '#/components/parameters/physicalNetworkIDParameter' + get: + description: Get a phyiscal network. + security: + - oauth2Authentication: [] + responses: + '200': + $ref: '#/components/responses/physicalNetworkResponse' + '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 a new physical network. + security: + - oauth2Authentication: [] + responses: + '202': + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/responses/acceptedResponse' + '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' components: parameters: organizationIDParameter: @@ -234,6 +274,13 @@ components: required: true schema: $ref: '#/components/schemas/kubernetesNameParameter' + physicalNetworkIDParameter: + name: physicalNetworkID + in: path + description: The physical network identifier. + required: true + schema: + $ref: '#/components/schemas/kubernetesNameParameter' schemas: kubernetesNameParameter: description: A Kubernetes name. Must be a valid DNS containing only lower case characters, numbers or hyphens, start and end with a character or number, and be at most 63 characters in length. @@ -511,12 +558,66 @@ components: type: array items: $ref: '#/components/schemas/identityRead' - physicalNetworkSpec: + ipv4Address: + description: An IPv4 address. + type: string + ipv4AddressList: + description: A list of IPv4 addresses. + type: array + items: + $ref: "#/components/schemas/ipv4Address" + physicalNetworkReadSpec: + description: A phyical network's specification. + type: object + required: + - regionId + - prefix + - dnsNameservers + - type + properties: + tags: + $ref: '#/components/schemas/tagList' + prefix: + description: An IPv4 prefix for the network. + type: string + dnsNameservers: + $ref: '#/components/schemas/ipv4AddressList' + type: + $ref: '#/components/schemas/regionType' + regionId: + description: The region an identity is provisioned in. + type: string + openstack: + $ref: '#/components/schemas/physicalNetworkSpecOpenstack' + physicalNetworkSpecOpenstack: + description: An openstack physical network. + properties: + vlanId: + description: The allocated VLAN ID. + type: integer + networkId: + description: The openstack network ID. + type: string + subnetId: + description: The openstack subnet ID. + type: string + routerId: + description: The openstack router ID. + type: string + physicalNetworkWriteSpec: description: A phyical network's specification. type: object + required: + - prefix + - dnsNameservers properties: tags: $ref: '#/components/schemas/tagList' + prefix: + description: An IPv4 prefix for the network. + type: string + dnsNameservers: + $ref: '#/components/schemas/ipv4AddressList' physicalNetworkWrite: description: A physical network request. type: object @@ -526,7 +627,7 @@ components: metadata: $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/resourceWriteMetadata' spec: - $ref: '#/components/schemas/physicalNetworkSpec' + $ref: '#/components/schemas/physicalNetworkWriteSpec' physicalNetworkRead: description: A physical network. type: object @@ -536,7 +637,7 @@ components: metadata: $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/projectScopedResourceReadMetadata' spec: - $ref: '#/components/schemas/physicalNetworkSpec' + $ref: '#/components/schemas/physicalNetworkReadSpec' externalNetwork: description: An Openstack external network. type: object @@ -586,6 +687,9 @@ components: tags: - name: cluster-id value: 9361402c-f998-49cc-ab21-9bb99afcfde8 + prefix: 192.168.0.0/24 + dnsNameservers: + - 8.8.8.8 responses: regionsResponse: description: A list of regions. @@ -700,8 +804,6 @@ components: schema: $ref: '#/components/schemas/physicalNetworkRead' example: - # TODO: metadata for region? - # TODO: metadata for identity? metadata: id: a64f9269-36e0-4312-b8d1-52d93d569b7b name: unused @@ -710,6 +812,15 @@ components: creationTime: 2024-05-31T14:11:00Z createdBy: john.doe@acme.com provisioningStatus: provisioned + spec: + regionId: d891dbf0-0a01-4efc-ae3a-5d77f6d3424b + prefix: 192.168.0.0/24 + dnsNameservers: + - 8.8.8.8 + type: openstack + openstack: + vlanId: 1024 + networkId: 824e8c4c-5858-4b02-9906-390e34561bf3 externalNetworksResponse: description: A list of valid external networks. content: diff --git a/pkg/openapi/types.go b/pkg/openapi/types.go index cd9a2ba..3044ae0 100644 --- a/pkg/openapi/types.go +++ b/pkg/openapi/types.go @@ -207,6 +207,12 @@ type ImageVirtualization string // Images A list of images that are compatible with this platform. type Images = []Image +// Ipv4Address An IPv4 address. +type Ipv4Address = string + +// Ipv4AddressList A list of IPv4 addresses. +type Ipv4AddressList = []Ipv4Address + // KubernetesNameParameter A Kubernetes name. Must be a valid DNS containing only lower case characters, numbers or hyphens, start and end with a character or number, and be at most 63 characters in length. type KubernetesNameParameter = string @@ -215,13 +221,43 @@ type PhysicalNetworkRead struct { Metadata externalRef0.ProjectScopedResourceReadMetadata `json:"metadata"` // Spec A phyical network's specification. - Spec *PhysicalNetworkSpec `json:"spec,omitempty"` + Spec *PhysicalNetworkReadSpec `json:"spec,omitempty"` } -// PhysicalNetworkSpec A phyical network's specification. -type PhysicalNetworkSpec struct { +// PhysicalNetworkReadSpec A phyical network's specification. +type PhysicalNetworkReadSpec struct { + // DnsNameservers A list of IPv4 addresses. + DnsNameservers Ipv4AddressList `json:"dnsNameservers"` + + // Openstack An openstack physical network. + Openstack *PhysicalNetworkSpecOpenstack `json:"openstack,omitempty"` + + // Prefix An IPv4 prefix for the network. + Prefix string `json:"prefix"` + + // RegionId The region an identity is provisioned in. + RegionId string `json:"regionId"` + // Tags A list of tags. Tags *TagList `json:"tags,omitempty"` + + // Type The region's provider type. + Type RegionType `json:"type"` +} + +// PhysicalNetworkSpecOpenstack An openstack physical network. +type PhysicalNetworkSpecOpenstack struct { + // NetworkId The openstack network ID. + NetworkId *string `json:"networkId,omitempty"` + + // RouterId The openstack router ID. + RouterId *string `json:"routerId,omitempty"` + + // SubnetId The openstack subnet ID. + SubnetId *string `json:"subnetId,omitempty"` + + // VlanId The allocated VLAN ID. + VlanId *int `json:"vlanId,omitempty"` } // PhysicalNetworkWrite A physical network request. @@ -230,7 +266,19 @@ type PhysicalNetworkWrite struct { Metadata externalRef0.ResourceWriteMetadata `json:"metadata"` // Spec A phyical network's specification. - Spec *PhysicalNetworkSpec `json:"spec,omitempty"` + Spec *PhysicalNetworkWriteSpec `json:"spec,omitempty"` +} + +// PhysicalNetworkWriteSpec A phyical network's specification. +type PhysicalNetworkWriteSpec struct { + // DnsNameservers A list of IPv4 addresses. + DnsNameservers Ipv4AddressList `json:"dnsNameservers"` + + // Prefix An IPv4 prefix for the network. + Prefix string `json:"prefix"` + + // Tags A list of tags. + Tags *TagList `json:"tags,omitempty"` } // RegionFeatures A set of features the region may provide to clients. @@ -290,6 +338,9 @@ type IdentityIDParameter = KubernetesNameParameter // OrganizationIDParameter defines model for organizationIDParameter. type OrganizationIDParameter = string +// PhysicalNetworkIDParameter A Kubernetes name. Must be a valid DNS containing only lower case characters, numbers or hyphens, start and end with a character or number, and be at most 63 characters in length. +type PhysicalNetworkIDParameter = KubernetesNameParameter + // ProjectIDParameter A Kubernetes name. Must be a valid DNS containing only lower case characters, numbers or hyphens, start and end with a character or number, and be at most 63 characters in length. type ProjectIDParameter = KubernetesNameParameter @@ -326,5 +377,5 @@ type PhysicalNetworkRequest = PhysicalNetworkWrite // PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesJSONRequestBody defines body for PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentities for application/json ContentType. type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesJSONRequestBody = IdentityWrite -// PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody defines body for PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworks for application/json ContentType. -type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody = PhysicalNetworkWrite +// PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody defines body for PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworks for application/json ContentType. +type PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityIDPhysicalnetworksJSONRequestBody = PhysicalNetworkWrite diff --git a/pkg/providers/allocation/vlan/vlan.go b/pkg/providers/allocation/vlan/vlan.go new file mode 100644 index 0000000..90c81e1 --- /dev/null +++ b/pkg/providers/allocation/vlan/vlan.go @@ -0,0 +1,175 @@ +/* +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 vlan + +import ( + "context" + "errors" + "fmt" + "slices" + + unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" + + kerrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +var ( + ErrAllocation = errors.New("allocation failure") +) + +type Allocator struct { + client client.Client + namespace string + name string + spec *unikornv1.VLANSpec +} + +func New(client client.Client, namespace, name string, spec *unikornv1.VLANSpec) *Allocator { + return &Allocator{ + client: client, + namespace: namespace, + name: name, + spec: spec, + } +} + +func (a *Allocator) allocatable() [4096]bool { + var table [4096]bool + + if a.spec == nil { + for i := 1; i < 4095; i++ { + table[i] = true + } + + return table + } + + for _, segment := range a.spec.Segments { + for i := segment.StartID; i <= segment.EndID; i++ { + table[i] = true + } + } + + return table +} + +func (a *Allocator) getVLANAllocation(ctx context.Context) (*unikornv1.VLANAllocation, error) { + allocation := &unikornv1.VLANAllocation{} + + if err := a.client.Get(ctx, client.ObjectKey{Namespace: a.namespace, Name: a.name}, allocation); err != nil { + return nil, err + } + + return allocation, nil +} + +func (a *Allocator) getOrCreateVLANAllocation(ctx context.Context) (*unikornv1.VLANAllocation, bool, error) { + create := false + + allocation, err := a.getVLANAllocation(ctx) + if err != nil { + if !kerrors.IsNotFound(err) { + return nil, false, err + } + + allocation = &unikornv1.VLANAllocation{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: a.namespace, + Name: a.name, + }, + } + + create = true + } + + return allocation, create, nil +} + +func (a *Allocator) Allocate(ctx context.Context, physicalNetworkID string) (int, error) { + allocation, create, err := a.getOrCreateVLANAllocation(ctx) + if err != nil { + return -1, err + } + + allocatable := a.allocatable() + + // Do an exhaustive search through all allocatable VLAN IDs... + // TODO: this is a O(n^2) problem, admitedly bounded. + for id := 1; id < 4095; id++ { + if !allocatable[id] { + continue + } + + // If there is already an allocation for that ID keep going.... + callback := func(allocation unikornv1.VLANAllocationEntry) bool { + return allocation.ID == id + } + + if index := slices.IndexFunc(allocation.Spec.Allocations, callback); index >= 0 { + continue + } + + // Perform an atomic update of the allocation table. + allocation.Spec.Allocations = append(allocation.Spec.Allocations, unikornv1.VLANAllocationEntry{ + ID: id, + PhysicalNetworkID: physicalNetworkID, + }) + + if create { + if err := a.client.Create(ctx, allocation); err != nil { + return -1, err + } + + return id, nil + } + + if err := a.client.Update(ctx, allocation); err != nil { + return -1, err + } + + return id, nil + } + + return -1, fmt.Errorf("%w: vlan ids exhausted", ErrAllocation) +} + +func (a *Allocator) Free(ctx context.Context, id int) error { + allocation, err := a.getVLANAllocation(ctx) + if err != nil { + return err + } + + allocationsLength := len(allocation.Spec.Allocations) + + callback := func(allocation unikornv1.VLANAllocationEntry) bool { + return allocation.ID == id + } + + allocation.Spec.Allocations = slices.DeleteFunc(allocation.Spec.Allocations, callback) + + if len(allocation.Spec.Allocations) != allocationsLength-1 { + return fmt.Errorf("%w: vlan id %d not allocated exactly once", ErrAllocation, id) + } + + if err := a.client.Update(ctx, allocation); err != nil { + return err + } + + return nil +} diff --git a/pkg/providers/interfaces.go b/pkg/providers/interfaces.go index 2770504..42fbbe4 100644 --- a/pkg/providers/interfaces.go +++ b/pkg/providers/interfaces.go @@ -20,7 +20,6 @@ import ( "context" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" - "github.com/unikorn-cloud/region/pkg/openapi" ) // Providers are expected to provide a provider agnostic manner. @@ -37,8 +36,10 @@ type Provider interface { 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. - CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, request *openapi.PhysicalNetworkWrite) (*unikornv1.PhysicalNetwork, error) + // CreatePhysicalNetwork creates a new physical network. + CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, physicalNetwork *unikornv1.PhysicalNetwork) error + // DeletePhysicalNetwork deletes a physical network. + DeletePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, physicalNetwork *unikornv1.PhysicalNetwork) error // ListExternalNetworks returns a list of external networks if the platform // supports such a concept. ListExternalNetworks(ctx context.Context) (ExternalNetworks, error) diff --git a/pkg/providers/openstack/network.go b/pkg/providers/openstack/network.go index c4ab69f..48a416c 100644 --- a/pkg/providers/openstack/network.go +++ b/pkg/providers/openstack/network.go @@ -20,14 +20,17 @@ package openstack import ( "context" "errors" + "fmt" "slices" "time" - "github.com/gophercloud/gophercloud/v2" + gophercloud "github.com/gophercloud/gophercloud/v2" "github.com/gophercloud/gophercloud/v2/openstack" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/external" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/layer3/routers" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/extensions/provider" "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/networks" + "github.com/gophercloud/gophercloud/v2/openstack/networking/v2/subnets" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" @@ -47,10 +50,13 @@ var ( // NetworkClient wraps the generic client because gophercloud is unsafe. type NetworkClient struct { + // client is a network client scoped as per the provider given + // during initialization. client *gophercloud.ServiceClient - + // options are optional configuration about the network service. options *unikornv1.RegionOpenstackNetworkSpec - + // externalNetworkCache provides caching to avoid having to talk to + // OpenStack. externalNetworkCache *cache.TimeoutCache[[]networks.Network] } @@ -147,43 +153,12 @@ func (c *NetworkClient) ExternalNetworks(ctx context.Context) ([]networks.Networ return result, nil } -// AllocateVLAN does exactly that using configured ID ranges and existing networks. -func (c *NetworkClient) AllocateVLAN(ctx context.Context) (int, error) { - allocatable := make([]bool, 4096) - - // If no configuration is given, own all of the IDs. If there are a list - // of segments, only allow those. - if c.options == nil || c.options.ProviderNetworks == nil || c.options.ProviderNetworks.VLAN == nil || c.options.ProviderNetworks.VLAN.Segments == nil { - for i := 1; i < 4096; i++ { - allocatable[i] = true - } - } else { - for _, segment := range c.options.ProviderNetworks.VLAN.Segments { - for i := segment.StartID; i < segment.EndID+1; i++ { - allocatable[i] = true - } - } - } - - // TODO: Next remove the ones we know are already allocated. - for i := range allocatable { - if allocatable[i] { - return i, nil - } - } - - return -1, ErrUnsufficentResource -} - // CreateVLANProviderNetwork creates a VLAN provider network for a project. -func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name string, projectID string) (int, *networks.Network, error) { +// This requires https://github.com/unikorn-cloud/python-unikorn-openstack-policy +// to be installed, see the README for further details on how this has to work. +func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name string, vlanID int) (*networks.Network, error) { if c.options == nil || c.options.ProviderNetworks == nil || c.options.ProviderNetworks.PhysicalNetwork == nil { - return -1, nil, ErrConfiguration - } - - vlanID, err := c.AllocateVLAN(ctx) - if err != nil { - return -1, nil, err + return nil, ErrConfiguration } tracer := otel.GetTracerProvider().Tracer(constants.Application) @@ -194,8 +169,7 @@ func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name stri opts := &provider.CreateOptsExt{ CreateOptsBuilder: &networks.CreateOpts{ Name: name, - Description: "unikorn provider network", - ProjectID: projectID, + Description: "unikorn managed provider network", }, Segments: []provider.Segment{ { @@ -208,8 +182,115 @@ func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name stri network, err := networks.Create(ctx, c.client, opts).Extract() if err != nil { - return -1, nil, err + return nil, err + } + + return network, nil +} + +func (c *NetworkClient) DeleteVLANProviderNetwork(ctx context.Context, id string) error { + if c.options == nil || c.options.ProviderNetworks == nil || c.options.ProviderNetworks.PhysicalNetwork == nil { + return ErrConfiguration + } + + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, fmt.Sprintf("DELETE /network/v2.0/networks/%s", id), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + return networks.Delete(ctx, c.client, id).ExtractErr() +} + +func (c *NetworkClient) CreateSubnet(ctx context.Context, name, networkID, prefix string, dnsNameservers []string) (*subnets.Subnet, error) { + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, "POST /network/v2.0/subnets", trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + opts := &subnets.CreateOpts{ + Name: name, + Description: "unikorn managed subnet", + NetworkID: networkID, + IPVersion: gophercloud.IPv4, + CIDR: prefix, + DNSNameservers: dnsNameservers, + } + + subnet, err := subnets.Create(ctx, c.client, opts).Extract() + if err != nil { + return nil, err + } + + return subnet, nil +} + +func (c *NetworkClient) DeleteSubnet(ctx context.Context, id string) error { + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, fmt.Sprintf("DELETE /network/v2.0/subnets/%s", id), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + return subnets.Delete(ctx, c.client, id).ExtractErr() +} + +func (c *NetworkClient) CreateRouter(ctx context.Context, name string) (*routers.Router, error) { + externalNetworks, err := c.ExternalNetworks(ctx) + if err != nil { + return nil, err + } + + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, "POST /network/v2.0/routers", trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + opts := &routers.CreateOpts{ + Name: name, + Description: "unikorn managed router", + GatewayInfo: &routers.GatewayInfo{ + NetworkID: externalNetworks[0].ID, + }, + } + + router, err := routers.Create(ctx, c.client, opts).Extract() + if err != nil { + return nil, err + } + + return router, nil +} + +func (c *NetworkClient) DeleteRouter(ctx context.Context, id string) error { + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, fmt.Sprintf("DELETE /network/v2.0/routers/%s", id), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + return routers.Delete(ctx, c.client, id).ExtractErr() +} + +func (c *NetworkClient) AddRouterInterface(ctx context.Context, routerID, subnetID string) error { + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, fmt.Sprintf("PUT /network/v2.0/routers/%s/add_router_interface", routerID), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + opts := &routers.AddInterfaceOpts{ + SubnetID: subnetID, + } + + return routers.AddInterface(ctx, c.client, routerID, opts).Err +} + +func (c *NetworkClient) RemoveRouterInterface(ctx context.Context, routerID, subnetID string) error { + tracer := otel.GetTracerProvider().Tracer(constants.Application) + + _, span := tracer.Start(ctx, fmt.Sprintf("PUT /network/v2.0/routers/%s/remove_router_interface", routerID), trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + opts := &routers.RemoveInterfaceOpts{ + SubnetID: subnetID, } - return vlanID, network, nil + return routers.RemoveInterface(ctx, c.client, routerID, opts).Err } diff --git a/pkg/providers/openstack/network_test.go b/pkg/providers/openstack/network_test.go deleted file mode 100644 index fa871a4..0000000 --- a/pkg/providers/openstack/network_test.go +++ /dev/null @@ -1,71 +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 openstack_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/require" - - unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" - "github.com/unikorn-cloud/region/pkg/providers/openstack" -) - -func TestVLANAllocateRanges(t *testing.T) { - tests := []struct { - name string - options *unikornv1.RegionOpenstackNetworkSpec - fail bool - expectedID int - }{ - { - name: "DefaultRange", - expectedID: 1, - }, - { - name: "SingleSegment", - options: &unikornv1.RegionOpenstackNetworkSpec{ - ProviderNetworks: &unikornv1.ProviderNetworks{ - VLAN: &unikornv1.VLANSpec{ - Segments: []unikornv1.VLANSegment{ - { - StartID: 100, - EndID: 200, - }, - }, - }, - }, - }, - expectedID: 100, - }, - } - - for _, test := range tests { - t.Run(t.Name()+"_"+test.name, func(t *testing.T) { - client := openstack.NewTestNetworkClient(test.options) - - id, err := client.AllocateVLAN(context.Background()) - if err != nil { - require.True(t, test.fail) - return - } - - require.Equal(t, test.expectedID, id) - }) - } -} diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index dd88e70..4eee47c 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -29,12 +29,10 @@ import ( "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" + "github.com/unikorn-cloud/region/pkg/providers/allocation/vlan" corev1 "k8s.io/api/core/v1" kerrors "k8s.io/apimachinery/pkg/api/errors" @@ -51,6 +49,14 @@ var ( ErrKeyUndefined = errors.New("a required key was not defined") ) +type providerCredentials struct { + endpoint string + domainID string + projectID string + userID string + password string +} + type Provider struct { // client is Kubernetes client. client client.Client @@ -61,10 +67,14 @@ type Provider struct { // secret is the current region secret. secret *corev1.Secret - domainID string - projectID string - userID string - password string + // credentials hold cloud identity information. + credentials *providerCredentials + + // vlan allocation table. + // NOTE: this can only be used by a single client unless it's moved + // into a Kubernetes resource of some variety to gain speculative locking + // powers. + vlanAllocator *vlan.Allocator // DO NOT USE DIRECTLY, CALL AN ACCESSOR. _identity *IdentityClient @@ -77,11 +87,24 @@ type Provider struct { var _ providers.Provider = &Provider{} -func New(client client.Client, region *unikornv1.Region) *Provider { - return &Provider{ - client: client, - region: region, +func New(ctx context.Context, cli client.Client, region *unikornv1.Region) (*Provider, error) { + var vlanSpec *unikornv1.VLANSpec + + if region.Spec.Openstack != nil && region.Spec.Openstack.Network != nil && region.Spec.Openstack.Network.ProviderNetworks != nil { + vlanSpec = region.Spec.Openstack.Network.ProviderNetworks.VLAN } + + p := &Provider{ + client: cli, + region: region, + vlanAllocator: vlan.New(cli, region.Namespace, "openstack-region-provider", vlanSpec), + } + + if err := p.serviceClientRefresh(ctx); err != nil { + return nil, err + } + + return p, nil } // serviceClientRefresh updates clients if they need to e.g. in the event @@ -147,15 +170,25 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error { return fmt.Errorf("%w: project-id", ErrKeyUndefined) } - // 'Regular' client calls to APIs for Nova, Glance etc. must to be project-scoped - providerClient := NewPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(projectID)) + credentials := &providerCredentials{ + endpoint: region.Spec.Openstack.Endpoint, + domainID: string(domainID), + projectID: string(projectID), + userID: string(userID), + password: string(password), + } - // Identity client is scoped to a domain to use the manager role + // The identity client needs to have "manager" powers, so it create projects and + // users within a domain without full admin. identity, err := NewIdentityClient(ctx, NewDomainScopedPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(domainID))) if err != nil { return err } + // Everything else gets a default view when bound to a project as a "member". + // Sadly, domain scoped accesses do not work by default any longer. + providerClient := NewPasswordProvider(region.Spec.Openstack.Endpoint, string(userID), string(password), string(projectID)) + compute, err := NewComputeClient(ctx, providerClient, region.Spec.Openstack.Compute) if err != nil { return err @@ -174,12 +207,7 @@ func (p *Provider) serviceClientRefresh(ctx context.Context) error { // Save the current configuration for checking next time. p.region = region p.secret = secret - - p.domainID = string(domainID) - p.projectID = string(projectID) - - p.userID = string(userID) - p.password = string(password) + p.credentials = credentials // Seve the clients p._identity = identity @@ -396,7 +424,7 @@ func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityC name := identityResourceName(identity) password := string(uuid.NewUUID()) - user, err := identityService.CreateUser(ctx, p.domainID, name, password) + user, err := identityService.CreateUser(ctx, p.credentials.domainID, name, password) if err != nil { return err } @@ -417,7 +445,7 @@ func (p *Provider) provisionProject(ctx context.Context, identityService *Identi name := identityResourceName(identity) - project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(identity)) + project, err := identityService.CreateProject(ctx, p.credentials.domainID, name, projectTags(identity)) if err != nil { return err } @@ -439,9 +467,19 @@ func roleNameToID(roles []roles.Role, name string) (string, error) { return "", fmt.Errorf("%w: role %s", ErrResourceNotFound, name) } -// getRequiredRoles returns the roles required for a user to create, manage and delete +// getRequiredProjectManagerRoles returns the roles required for a manager to create, manage +// and delete things like provider networks to support baremetal. +func (p *Provider) getRequiredProjectManagerRoles() []string { + defaultRoles := []string{ + "manager", + } + + return defaultRoles +} + +// getRequiredProjectUserRoles returns the roles required for a user to create, manage and delete // a cluster. -func (p *Provider) getRequiredRoles() []string { +func (p *Provider) getRequiredProjectUserRoles() []string { if p.region.Spec.Openstack.Identity != nil && len(p.region.Spec.Openstack.Identity.ClusterRoles) > 0 { return p.region.Spec.Openstack.Identity.ClusterRoles } @@ -457,19 +495,19 @@ 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, identity *unikornv1.OpenstackIdentity) error { +func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity, userID string, rolesGetter func() []string) error { allRoles, err := identityService.ListRoles(ctx) if err != nil { return err } - for _, name := range p.getRequiredRoles() { + for _, name := range rolesGetter() { roleID, err := roleNameToID(allRoles, name) if err != nil { return err } - if err := identityService.CreateRoleAssignment(ctx, *identity.Spec.UserID, *identity.Spec.ProjectID, roleID); err != nil { + if err := identityService.CreateRoleAssignment(ctx, userID, *identity.Spec.ProjectID, roleID); err != nil { return err } } @@ -492,7 +530,7 @@ func (p *Provider) provisionApplicationCredential(ctx context.Context, identity name := identityResourceName(identity) - appcred, err := identityService.CreateApplicationCredential(ctx, *identity.Spec.UserID, name, "IaaS lifecycle management", p.getRequiredRoles()) + appcred, err := identityService.CreateApplicationCredential(ctx, *identity.Spec.UserID, name, "IaaS lifecycle management", p.getRequiredProjectUserRoles()) if err != nil { return err } @@ -534,29 +572,6 @@ func (p *Provider) createClientConfig(identity *unikornv1.OpenstackIdentity) err return nil } -func convertTag(in openapi.Tag) unikornv1.Tag { - out := unikornv1.Tag{ - Name: in.Name, - Value: in.Value, - } - - return out -} - -func convertTagList(in *openapi.TagList) unikornv1.TagList { - if in == nil { - return nil - } - - out := make(unikornv1.TagList, len(*in)) - - for i := range *in { - out[i] = convertTag((*in)[i]) - } - - return out -} - func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unikornv1.OpenstackIdentity) error { if identity.Spec.ServerGroupID != nil { return nil @@ -634,6 +649,7 @@ func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Ident return err } + // Always attempt to record where we are up to for idempotency. record := func() { log := log.FromContext(ctx) @@ -658,6 +674,14 @@ func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Ident return err } + // Grant the "manager" role on the project for unikorn's user. Sadly when provisioning + // resources, most services can only infer the project ID from the token, and not any + // of the heirarchy, so we cannot define policy rules for a domain manager in the same + // way as can be done for the identity service. + if err := p.provisionProjectRoles(ctx, identityService, openstackIdentity, p.credentials.userID, p.getRequiredProjectManagerRoles); 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. @@ -667,7 +691,7 @@ func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Ident // Give the user only what permissions they need to provision a cluster and // manage it during its lifetime. - if err := p.provisionProjectRoles(ctx, identityService, openstackIdentity); err != nil { + if err := p.provisionProjectRoles(ctx, identityService, openstackIdentity, *openstackIdentity.Spec.UserID, p.getRequiredProjectUserRoles); err != nil { return err } @@ -700,7 +724,24 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident return nil } - if openstackIdentity.Spec.UserID != nil && openstackIdentity.Spec.ProjectID != nil { + complete := false + + // Always attempt to record where we are up to for idempotency. + record := func() { + if complete { + return + } + + log := log.FromContext(ctx) + + if err := p.client.Update(ctx, openstackIdentity); err != nil { + log.Error(err, "failed to update openstack identity") + } + } + + defer record() + + if openstackIdentity.Spec.ServerGroupID != nil { // Rescope to the user/project... providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *openstackIdentity.Spec.UserID, *openstackIdentity.Spec.Password, *openstackIdentity.Spec.ProjectID) @@ -709,11 +750,11 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident return err } - if openstackIdentity.Spec.ServerGroupID != nil { - if err := computeService.DeleteServerGroup(ctx, *openstackIdentity.Spec.ServerGroupID); err != nil { - return err - } + if err := computeService.DeleteServerGroup(ctx, *openstackIdentity.Spec.ServerGroupID); err != nil { + return err } + + openstackIdentity.Spec.ServerGroupID = nil } identityService, err := p.identity(ctx) @@ -725,65 +766,298 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident if err := identityService.DeleteUser(ctx, *openstackIdentity.Spec.UserID); err != nil { return err } + + openstackIdentity.Spec.UserID = nil } if openstackIdentity.Spec.ProjectID != nil { if err := identityService.DeleteProject(ctx, *openstackIdentity.Spec.ProjectID); err != nil { return err } + + openstackIdentity.Spec.ProjectID = nil } if err := p.client.Delete(ctx, openstackIdentity); err != nil { return err } + complete = true + + return nil +} + +func (p *Provider) GetOpenstackPhysicalNetwork(ctx context.Context, physicalNetwork *unikornv1.PhysicalNetwork) (*unikornv1.OpenstackPhysicalNetwork, error) { + var result unikornv1.OpenstackPhysicalNetwork + + if err := p.client.Get(ctx, client.ObjectKey{Namespace: physicalNetwork.Namespace, Name: physicalNetwork.Name}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (p *Provider) GetOrCreateOpenstackPhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, physicalNetwork *unikornv1.PhysicalNetwork) (*unikornv1.OpenstackPhysicalNetwork, bool, error) { + create := false + + openstackPhysicalNetwork, err := p.GetOpenstackPhysicalNetwork(ctx, physicalNetwork) + if err != nil { + if !kerrors.IsNotFound(err) { + return nil, false, err + } + + openstackPhysicalNetwork = &unikornv1.OpenstackPhysicalNetwork{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: physicalNetwork.Namespace, + Name: physicalNetwork.Name, + Labels: map[string]string{ + constants.IdentityLabel: identity.Name, + constants.PhysicalNetworkLabel: physicalNetwork.Name, + }, + Annotations: physicalNetwork.Annotations, + }, + } + + for k, v := range physicalNetwork.Labels { + openstackPhysicalNetwork.Labels[k] = v + } + + create = true + } + + return openstackPhysicalNetwork, create, nil +} + +func (p *Provider) allocateVLAN(ctx context.Context, physicalNetwork *unikornv1.OpenstackPhysicalNetwork) error { + if physicalNetwork.Spec.VlanID != nil { + return nil + } + + vlanID, err := p.vlanAllocator.Allocate(ctx, physicalNetwork.Name) + if err != nil { + return err + } + + physicalNetwork.Spec.VlanID = &vlanID + + return nil +} + +func (p *Provider) createPhysicalNetwork(ctx context.Context, networkService *NetworkClient, identity *unikornv1.OpenstackIdentity, physicalNetwork *unikornv1.OpenstackPhysicalNetwork) error { + if physicalNetwork.Spec.NetworkID != nil { + return nil + } + + providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "unikorn-openstack-region-provider-network", *physicalNetwork.Spec.VlanID) + if err != nil { + return err + } + + physicalNetwork.Spec.NetworkID = &providerNetwork.ID + + return nil +} + +func (p *Provider) createSubnet(ctx context.Context, networkService *NetworkClient, physicalNetwork *unikornv1.PhysicalNetwork, openstackPhysicalNetwork *unikornv1.OpenstackPhysicalNetwork) error { + if openstackPhysicalNetwork.Spec.SubnetID != nil { + return nil + } + + dnsNameservers := make([]string, len(physicalNetwork.Spec.DNSNameservers)) + + for i, ip := range physicalNetwork.Spec.DNSNameservers { + dnsNameservers[i] = ip.String() + } + + subnet, err := networkService.CreateSubnet(ctx, "unikorn-openstack-region-provider-subnet", *openstackPhysicalNetwork.Spec.NetworkID, physicalNetwork.Spec.Prefix.String(), dnsNameservers) + if err != nil { + return err + } + + openstackPhysicalNetwork.Spec.SubnetID = &subnet.ID + + return nil +} + +func (p *Provider) createRouter(ctx context.Context, networkService *NetworkClient, openstackPhysicalNetwork *unikornv1.OpenstackPhysicalNetwork) error { + if openstackPhysicalNetwork.Spec.RouterID != nil { + return nil + } + + router, err := networkService.CreateRouter(ctx, "unikorn-openstack-region-provider-router") + if err != nil { + return nil + } + + openstackPhysicalNetwork.Spec.RouterID = &router.ID + + return nil +} + +func (p *Provider) addRouterSubnetInterface(ctx context.Context, networkService *NetworkClient, openstackPhysicalNetwork *unikornv1.OpenstackPhysicalNetwork) error { + if openstackPhysicalNetwork.Spec.RouterSubnetInterfaceAdded { + return nil + } + + if err := networkService.AddRouterInterface(ctx, *openstackPhysicalNetwork.Spec.RouterID, *openstackPhysicalNetwork.Spec.SubnetID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.RouterSubnetInterfaceAdded = true + return nil } // CreatePhysicalNetwork creates a physical network for an identity. -func (p *Provider) CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, request *openapi.PhysicalNetworkWrite) (*unikornv1.PhysicalNetwork, error) { +func (p *Provider) CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, physicalNetwork *unikornv1.PhysicalNetwork) error { openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) if err != nil { - return nil, err + return err } - networkService, err := p.network(ctx) + openstackPhysicalNetwork, create, err := p.GetOrCreateOpenstackPhysicalNetwork(ctx, identity, physicalNetwork) if err != nil { - return nil, err + return err + } + + // Always attempt to record where we are up to for idempotency. + record := func() { + log := log.FromContext(ctx) + + if create { + if err := p.client.Create(ctx, openstackPhysicalNetwork); err != nil { + log.Error(err, "failed to create openstack physical network") + } + + return + } + + if err := p.client.Update(ctx, openstackPhysicalNetwork); err != nil { + log.Error(err, "failed to update openstack physical network") + } } - vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "cluster-provider-network", *openstackIdentity.Spec.ProjectID) + defer record() + + if err := p.allocateVLAN(ctx, openstackPhysicalNetwork); err != nil { + return err + } + + // Rescope to the project... + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, p.credentials.userID, p.credentials.password, *openstackIdentity.Spec.ProjectID) + + networkService, err := NewNetworkClient(ctx, providerClient, p.region.Spec.Openstack.Network) if err != nil { - return nil, err + return err + } + + if err := p.createPhysicalNetwork(ctx, networkService, openstackIdentity, openstackPhysicalNetwork); err != nil { + return err } - userinfo, err := authorization.UserinfoFromContext(ctx) + if err := p.createSubnet(ctx, networkService, physicalNetwork, openstackPhysicalNetwork); err != nil { + return err + } + + if err := p.createRouter(ctx, networkService, openstackPhysicalNetwork); err != nil { + return err + } + + if err := p.addRouterSubnetInterface(ctx, networkService, openstackPhysicalNetwork); err != nil { + return err + } + + return nil +} + +// DeletePhysicalNetwork deletes a physical network. +func (p *Provider) DeletePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, physicalNetwork *unikornv1.PhysicalNetwork) error { + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) if err != nil { - return nil, err + return err } - 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) + openstackPhysicalNetwork, err := p.GetOpenstackPhysicalNetwork(ctx, physicalNetwork) + if err != nil { + if !kerrors.IsNotFound(err) { + return err + } - physicalNetwork := &unikornv1.PhysicalNetwork{ - ObjectMeta: objectMeta.Get(), - Spec: unikornv1.PhysicalNetworkSpec{ - Tags: convertTagList(request.Spec.Tags), - ProviderNetwork: &unikornv1.OpenstackProviderNetworkSpec{ - ID: providerNetwork.ID, - VlanID: vlanID, - }, - }, + return nil } - if err := p.client.Create(ctx, physicalNetwork); err != nil { - return nil, err + complete := false + + // Always attempt to record where we are up to for idempotency. + record := func() { + if complete { + return + } + + log := log.FromContext(ctx) + + if err := p.client.Update(ctx, openstackPhysicalNetwork); err != nil { + log.Error(err, "failed to update openstack physical network") + } + } + + defer record() + + // Rescope to the project... + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, p.credentials.userID, p.credentials.password, *openstackIdentity.Spec.ProjectID) + + networkService, err := NewNetworkClient(ctx, providerClient, p.region.Spec.Openstack.Network) + if err != nil { + return err } - return physicalNetwork, nil + if openstackPhysicalNetwork.Spec.RouterSubnetInterfaceAdded { + if err := networkService.RemoveRouterInterface(ctx, *openstackPhysicalNetwork.Spec.RouterID, *openstackPhysicalNetwork.Spec.SubnetID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.RouterSubnetInterfaceAdded = false + } + + if openstackPhysicalNetwork.Spec.RouterID != nil { + if err := networkService.DeleteRouter(ctx, *openstackPhysicalNetwork.Spec.RouterID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.RouterID = nil + } + + if openstackPhysicalNetwork.Spec.SubnetID != nil { + if err := networkService.DeleteSubnet(ctx, *openstackPhysicalNetwork.Spec.SubnetID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.SubnetID = nil + } + + if openstackPhysicalNetwork.Spec.NetworkID != nil { + if err := networkService.DeleteVLANProviderNetwork(ctx, *openstackPhysicalNetwork.Spec.NetworkID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.NetworkID = nil + } + + if openstackPhysicalNetwork.Spec.VlanID != nil { + if err := p.vlanAllocator.Free(ctx, *openstackPhysicalNetwork.Spec.VlanID); err != nil { + return err + } + + openstackPhysicalNetwork.Spec.VlanID = nil + } + + if err := p.client.Delete(ctx, openstackPhysicalNetwork); err != nil { + return err + } + + complete = true + + return nil } // ListExternalNetworks returns a list of external networks if the platform diff --git a/pkg/provisioners/managers/identity/provisioner.go b/pkg/provisioners/managers/identity/provisioner.go index 980134e..db0e7da 100644 --- a/pkg/provisioners/managers/identity/provisioner.go +++ b/pkg/provisioners/managers/identity/provisioner.go @@ -26,6 +26,12 @@ import ( 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" + + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/selection" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" ) // Provisioner encapsulates control plane provisioning. @@ -71,11 +77,48 @@ func (p *Provisioner) Provision(ctx context.Context) error { // Deprovision implements the Provision interface. func (p *Provisioner) Deprovision(ctx context.Context) error { + log := log.FromContext(ctx) + cli, err := coreclient.ProvisionerClientFromContext(ctx) if err != nil { return err } + identityRequirement, err := labels.NewRequirement(constants.IdentityLabel, selection.Equals, []string{p.identity.Name}) + if err != nil { + return err + } + + selector := labels.NewSelector() + selector = selector.Add(*identityRequirement) + + // Block identity deletion until all owned resources are deleted, we cannot guarantee + // the underlying cloud implementation will not just orphan them and leak resources. + var physicalNetworks unikornv1.PhysicalNetworkList + + if err := cli.List(ctx, &physicalNetworks, &client.ListOptions{Namespace: p.identity.Namespace, LabelSelector: selector}); err != nil { + return err + } + + if len(physicalNetworks.Items) != 0 { + for i := range physicalNetworks.Items { + resource := &physicalNetworks.Items[i] + + if resource.DeletionTimestamp != nil { + log.Info("awaiting physical network deletion", "physical network", resource.Name) + continue + } + + log.Info("triggering physical network deletion", "physical network", resource.Name) + + if err := cli.Delete(ctx, resource); err != nil { + return err + } + } + + return provisioners.ErrYield + } + provider, err := region.NewClient(cli, p.identity.Namespace).Provider(ctx, p.identity.Labels[constants.RegionLabel]) if err != nil { return err diff --git a/pkg/provisioners/managers/physicalnetwork/provisioner.go b/pkg/provisioners/managers/physicalnetwork/provisioner.go new file mode 100644 index 0000000..ed24939 --- /dev/null +++ b/pkg/provisioners/managers/physicalnetwork/provisioner.go @@ -0,0 +1,140 @@ +/* +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 physicalnetwork + +import ( + "context" + "errors" + "fmt" + + 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" + + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +var ( + ErrResouceDependency = errors.New("resource dependency error") +) + +// Provisioner encapsulates control plane provisioning. +type Provisioner struct { + provisioners.Metadata + + // physicalNetwork is the physicalNetwork we're provisioning. + physicalNetwork *unikornv1.PhysicalNetwork +} + +// New returns a new initialized provisioner object. +func New(_ coremanager.ControllerOptions) provisioners.ManagerProvisioner { + return &Provisioner{ + physicalNetwork: &unikornv1.PhysicalNetwork{}, + } +} + +// Ensure the ManagerProvisioner interface is implemented. +var _ provisioners.ManagerProvisioner = &Provisioner{} + +func (p *Provisioner) Object() unikornv1core.ManagableResourceInterface { + return p.physicalNetwork +} + +func (p *Provisioner) getIdentity(ctx context.Context, cli client.Client) (*unikornv1.Identity, error) { + identity := &unikornv1.Identity{} + + if err := cli.Get(ctx, client.ObjectKey{Namespace: p.physicalNetwork.Namespace, Name: p.physicalNetwork.Labels[constants.IdentityLabel]}, identity); err != nil { + return nil, err + } + + return identity, nil +} + +// Provision implements the Provision interface. +func (p *Provisioner) Provision(ctx context.Context) error { + log := log.FromContext(ctx) + + cli, err := coreclient.ProvisionerClientFromContext(ctx) + if err != nil { + return err + } + + provider, err := region.NewClient(cli, p.physicalNetwork.Namespace).Provider(ctx, p.physicalNetwork.Labels[constants.RegionLabel]) + if err != nil { + return err + } + + identity, err := p.getIdentity(ctx, cli) + if err != nil { + return err + } + + // Inhibit provisioning until the identity is ready, as we may need the identity information + // to create the physical network e.g. the project ID in the case of OpenStack. + // TODO: the kinda mirrors what the Kubernetes service is doing when waiting for an identity + // and physical network, perhaps we can formalize and share the concepts? + status, err := identity.StatusConditionRead(unikornv1core.ConditionAvailable) + if err != nil { + log.Info("waiting for identity status update") + + return provisioners.ErrYield + } + + switch status.Reason { + case unikornv1core.ConditionReasonProvisioned: + break + case unikornv1core.ConditionReasonProvisioning: + return provisioners.ErrYield + default: + return fmt.Errorf("%w: identity in unexpected condition %v", ErrResouceDependency, status.Reason) + } + + if err := provider.CreatePhysicalNetwork(ctx, identity, p.physicalNetwork); 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.physicalNetwork.Namespace).Provider(ctx, p.physicalNetwork.Labels[constants.RegionLabel]) + if err != nil { + return err + } + + identity, err := p.getIdentity(ctx, cli) + if err != nil { + return err + } + + if err := provider.DeletePhysicalNetwork(ctx, identity, p.physicalNetwork); err != nil { + return err + } + + return nil +}