From 5204bb682cc0ddcad2d1c61d1bb26b072314abab Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Mon, 1 Jul 2024 13:24:52 +0100 Subject: [PATCH] Physical Network API Add in an API for physical network creation, adjust the identit API to keep things in whack with everything else (mostly). --- charts/region/Chart.yaml | 4 +- go.mod | 2 +- go.sum | 4 +- pkg/apis/unikorn/v1alpha1/register.go | 1 + pkg/apis/unikorn/v1alpha1/types.go | 56 ++++- .../unikorn/v1alpha1/zz_generated.deepcopy.go | 160 ++++++++++++++ pkg/constants/constants.go | 9 + pkg/handler/handler.go | 79 ++++++- pkg/openapi/client.go | 199 ++++++++++++++++++ pkg/openapi/router.go | 66 ++++++ pkg/openapi/schema.go | 154 +++++++------- pkg/openapi/server.spec.yaml | 144 ++++++++++++- pkg/openapi/types.go | 70 +++++- pkg/providers/interfaces.go | 5 +- pkg/providers/openstack/network.go | 13 +- pkg/providers/openstack/provider.go | 118 ++++++++--- pkg/providers/types.go | 15 +- 17 files changed, 955 insertions(+), 144 deletions(-) diff --git a/charts/region/Chart.yaml b/charts/region/Chart.yaml index 779b9d2..69d1338 100644 --- a/charts/region/Chart.yaml +++ b/charts/region/Chart.yaml @@ -4,8 +4,8 @@ description: A Helm chart for deploying Unikorn's Region Controller type: application -version: v0.1.17 -appVersion: v0.1.17 +version: v0.1.18 +appVersion: v0.1.18 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png diff --git a/go.mod b/go.mod index 8b1220b..8f44a47 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/oapi-codegen/runtime v1.1.1 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 - github.com/unikorn-cloud/core v0.1.49 + github.com/unikorn-cloud/core v0.1.55 github.com/unikorn-cloud/identity v0.2.11 go.opentelemetry.io/otel v1.27.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.27.0 diff --git a/go.sum b/go.sum index ed9e56d..089db18 100644 --- a/go.sum +++ b/go.sum @@ -136,8 +136,8 @@ 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.49 h1:ahAxrzvBnBICi+qN/AmTqKRJHpxl958gKVfBO3lz4G8= -github.com/unikorn-cloud/core v0.1.49/go.mod h1:cP39UQN7aSmsfjQuSMsworI4oBIwx4oA4u20CbPpfZw= +github.com/unikorn-cloud/core v0.1.55 h1:Oy5r3UBTNWb0qFDcmehLrgBwMx9xCo9x2nOEzNZoYUU= +github.com/unikorn-cloud/core v0.1.55/go.mod h1:cP39UQN7aSmsfjQuSMsworI4oBIwx4oA4u20CbPpfZw= github.com/unikorn-cloud/identity v0.2.11 h1:q6mkJ3qTRjwhlvLS9Jv0I4wlJhnsbJZHu2rbNdnXBYk= github.com/unikorn-cloud/identity v0.2.11/go.mod h1:4KHNdHiIKpKERD0slunDDXhdC59M7eiN+Y1wSfHbQwQ= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/pkg/apis/unikorn/v1alpha1/register.go b/pkg/apis/unikorn/v1alpha1/register.go index 4ad8e09..77de2bb 100644 --- a/pkg/apis/unikorn/v1alpha1/register.go +++ b/pkg/apis/unikorn/v1alpha1/register.go @@ -50,6 +50,7 @@ var ( func init() { SchemeBuilder.Register(&Region{}, &RegionList{}) SchemeBuilder.Register(&Identity{}, &IdentityList{}) + SchemeBuilder.Register(&PhysicalNetwork{}, &PhysicalNetworkList{}) } // 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 dc56655..2d6d67a 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -223,6 +223,17 @@ type RegionStatus struct { Conditions []unikornv1core.Condition `json:"conditions,omitempty"` } +// Tag is an arbirary key/value. +type Tag struct { + // Name of the tag. + Name string `json:"name"` + // Value of the tag. + Value string `json:"value"` +} + +// TagList is an ordered list of tags. +type TagList []Tag + // IdentityList is a typed list of identities. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object type IdentityList struct { @@ -252,6 +263,9 @@ type Identity struct { // IdentitySpec stores any state necessary to manage identity. type IdentitySpec struct { + // Tags are an abitrary list of key/value pairs that a client + // may populate to store metadata for the resource. + Tags TagList `json:"tags,omitempty"` // Provider defines the provider type. Provider Provider `json:"provider"` // OpenStack is populated when the provider type is set to "openstack". @@ -261,9 +275,49 @@ type IdentitySpec struct { type IdentitySpecOpenStack struct { // UserID is the ID of the user created for the identity. UserID string `json:"userID"` - // ProjectIS is the ID of the project created for the identity. + // ProjectID is the ID of the project created for the identity. ProjectID string `json:"projectID"` } type IdentityStatus struct { } + +// PhysicalNetworkList s a typed list of physical networks. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type PhysicalNetworkList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []PhysicalNetwork `json:"items"` +} + +// PhysicalNetwork 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="status",type="string",JSONPath=".status.conditions[?(@.type==\"Available\")].reason" +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +type PhysicalNetwork struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec PhysicalNetworkSpec `json:"spec"` + Status PhysicalNetworkStatus `json:"status"` +} + +type PhysicalNetworkSpec struct { + // 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"` +} + +type OpenstackProviderNetworkSpec struct { + // ID is the network ID. + ID string `json:"id"` + // VlanID is the ID if the VLAN for IPAM. + VlanID int `json:"vlanID"` +} + +type PhysicalNetworkStatus struct { +} diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index f6039a8..e9d0fce 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -132,6 +132,11 @@ func (in *IdentityList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IdentitySpec) DeepCopyInto(out *IdentitySpec) { *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(TagList, len(*in)) + copy(*out, *in) + } if in.OpenStack != nil { in, out := &in.OpenStack, &out.OpenStack *out = new(IdentitySpecOpenStack) @@ -268,6 +273,125 @@ func (in *OpenstackFlavorsSpec) DeepCopy() *OpenstackFlavorsSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackProviderNetworkSpec) DeepCopyInto(out *OpenstackProviderNetworkSpec) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackProviderNetworkSpec. +func (in *OpenstackProviderNetworkSpec) DeepCopy() *OpenstackProviderNetworkSpec { + if in == nil { + return nil + } + out := new(OpenstackProviderNetworkSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *PhysicalNetwork) DeepCopyInto(out *PhysicalNetwork) { + *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 PhysicalNetwork. +func (in *PhysicalNetwork) DeepCopy() *PhysicalNetwork { + if in == nil { + return nil + } + out := new(PhysicalNetwork) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PhysicalNetwork) 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 *PhysicalNetworkList) DeepCopyInto(out *PhysicalNetworkList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]PhysicalNetwork, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhysicalNetworkList. +func (in *PhysicalNetworkList) DeepCopy() *PhysicalNetworkList { + if in == nil { + return nil + } + out := new(PhysicalNetworkList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *PhysicalNetworkList) 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 *PhysicalNetworkSpec) DeepCopyInto(out *PhysicalNetworkSpec) { + *out = *in + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make(TagList, len(*in)) + copy(*out, *in) + } + if in.ProviderNetwork != nil { + in, out := &in.ProviderNetwork, &out.ProviderNetwork + *out = new(OpenstackProviderNetworkSpec) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhysicalNetworkSpec. +func (in *PhysicalNetworkSpec) DeepCopy() *PhysicalNetworkSpec { + if in == nil { + return nil + } + out := new(PhysicalNetworkSpec) + in.DeepCopyInto(out) + return out +} + +// 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 + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PhysicalNetworkStatus. +func (in *PhysicalNetworkStatus) DeepCopy() *PhysicalNetworkStatus { + if in == nil { + return nil + } + out := new(PhysicalNetworkStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Region) DeepCopyInto(out *Region) { *out = *in @@ -513,6 +637,42 @@ func (in *RegionStatus) DeepCopy() *RegionStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Tag) DeepCopyInto(out *Tag) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Tag. +func (in *Tag) DeepCopy() *Tag { + if in == nil { + return nil + } + out := new(Tag) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in TagList) DeepCopyInto(out *TagList) { + { + in := &in + *out = make(TagList, len(*in)) + copy(*out, *in) + return + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TagList. +func (in TagList) DeepCopy() TagList { + if in == nil { + return nil + } + out := new(TagList) + 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 a5c5765..66b9b9c 100644 --- a/pkg/constants/constants.go +++ b/pkg/constants/constants.go @@ -43,3 +43,12 @@ var ( func VersionString() string { return fmt.Sprintf("%s/%s (revision/%s)", Application, Version, Revision) } + +const ( + // RegionLabel creates an indexable linkage between resources and their + // owning region. + RegionLabel = "regions.unikorn-cloud.org/region-id" + // IdentityLabel creates an indexable linkage between resources and an + // owning identity. + IdentityLabel = "regions.unikorn-cloud.org/identity-id" +) diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index bfdd8a1..138060f 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -243,11 +243,38 @@ func (h *Handler) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsReg util.WriteJSONResponse(w, r, http.StatusOK, out) } -func convertCloudConfig(identity *unikornv1.Identity, in *providers.CloudConfig) *openapi.IdentityRead { +func convertTag(in unikornv1.Tag) openapi.Tag { + out := openapi.Tag{ + Name: in.Name, + Value: in.Value, + } + + return out +} + +func convertTags(in unikornv1.TagList) openapi.TagList { + if in == nil { + return nil + } + + out := make(openapi.TagList, len(in)) + + for i := range in { + out[i] = convertTag(in[i]) + } + + return out +} + +func convertIdentity(identity *unikornv1.Identity, in *providers.CloudConfig) *openapi.IdentityRead { out := &openapi.IdentityRead{ Metadata: conversion.ProjectScopedResourceReadMetadata(identity, coreopenapi.ResourceProvisioningStatusProvisioned), } + if tags := convertTags(identity.Spec.Tags); tags != nil { + out.Spec.Tags = &tags + } + switch in.Type { case providers.ProviderTypeOpenStack: out.Spec = openapi.IdentitySpec{ @@ -264,23 +291,54 @@ func convertCloudConfig(identity *unikornv1.Identity, in *providers.CloudConfig) return out } -func generateClusterInfo(organizationID, projectID string, in *openapi.IdentityWrite) *providers.ClusterInfo { - out := &providers.ClusterInfo{ - OrganizationID: organizationID, - ProjectID: projectID, - ClusterID: in.ClusterId, +func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, regionID openapi.RegionIDParameter) { + if err := h.checkRBAC(r.Context(), organizationID, "infrastructure", constants.Create); err != nil { + errors.HandleError(w, r, err) + return + } + + request := &openapi.IdentityWrite{} + + if err := util.ReadJSONBody(r, request); err != nil { + errors.HandleError(w, r, err) + return + } + + provider, err := region.NewClient(h.client, h.namespace).Provider(r.Context(), regionID) + if err != nil { + errors.HandleError(w, r, err) + return + } + + identity, cloudconfig, err := provider.CreateIdentity(r.Context(), organizationID, projectID, request) + if err != nil { + errors.HandleError(w, r, err) + return + } + + h.setCacheable(w) + util.WriteJSONResponse(w, r, http.StatusCreated, convertIdentity(identity, cloudconfig)) +} + +func convertPhysicalNetwork(in *unikornv1.PhysicalNetwork) *openapi.PhysicalNetworkRead { + out := &openapi.PhysicalNetworkRead{ + Metadata: conversion.ProjectScopedResourceReadMetadata(in, coreopenapi.ResourceProvisioningStatusProvisioned), + } + + if tags := convertTags(in.Spec.Tags); tags != nil { + out.Spec.Tags = &tags } return out } -func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, regionID openapi.RegionIDParameter) { +func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, regionID openapi.RegionIDParameter, identityID openapi.IdentityIDParameter) { if err := h.checkRBAC(r.Context(), organizationID, "infrastructure", constants.Create); err != nil { errors.HandleError(w, r, err) return } - request := &openapi.IdentityWrite{} + request := &openapi.PhysicalNetworkWrite{} if err := util.ReadJSONBody(r, request); err != nil { errors.HandleError(w, r, err) @@ -293,14 +351,13 @@ func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRe return } - identity, cloudconfig, err := provider.CreateIdentity(r.Context(), generateClusterInfo(organizationID, projectID, request)) + network, err := provider.CreatePhysicalNetwork(r.Context(), organizationID, projectID, identityID, request) if err != nil { errors.HandleError(w, r, err) return } - h.setCacheable(w) - util.WriteJSONResponse(w, r, http.StatusCreated, convertCloudConfig(identity, cloudconfig)) + util.WriteJSONResponse(w, r, http.StatusCreated, convertPhysicalNetwork(network)) } func convertExternalNetwork(in providers.ExternalNetwork) openapi.ExternalNetwork { diff --git a/pkg/openapi/client.go b/pkg/openapi/client.go index 009e014..eb795e4 100644 --- a/pkg/openapi/client.go +++ b/pkg/openapi/client.go @@ -104,6 +104,11 @@ type ClientInterface interface { PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBody request with any body + PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages request GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) } @@ -168,6 +173,30 @@ func (c *Client) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsReg return c.Client.Do(req) } +func (c *Client) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(c.Server, organizationID, projectID, regionID, 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) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequest(c.Server, organizationID, projectID, regionID, identityID, 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) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesRequest(c.Server, organizationID, projectID, regionID) if err != nil { @@ -378,6 +407,74 @@ func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIden return req, nil } +// NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequest calls the generic PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks builder with application/json body +func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(server, organizationID, projectID, regionID, identityID, "application/json", bodyReader) +} + +// NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody generates requests for PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks with any type of body +func NewPostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksRequestWithBody(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader) (*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, "regionID", runtime.ParamLocationPath, regionID) + if err != nil { + return nil, err + } + + var pathParam3 string + + pathParam3, err = runtime.StyleParamWithLocation("simple", false, "identityID", runtime.ParamLocationPath, identityID) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/api/v1/organizations/%s/projects/%s/regions/%s/identities/%s/physicalNetworks", pathParam0, pathParam1, pathParam2, pathParam3) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesRequest generates requests for GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages func NewGetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesRequest(server string, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter) (*http.Request, error) { var err error @@ -483,6 +580,11 @@ type ClientWithResponsesInterface interface { PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesResponse, error) + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse request with any body + PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse, error) + + PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse, error) + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesWithResponse request GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse, error) } @@ -588,6 +690,32 @@ func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIden return 0 } +type PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse struct { + Body []byte + HTTPResponse *http.Response + JSON201 *PhysicalNetworkResponse + JSON400 *externalRef0.BadRequestResponse + JSON401 *externalRef0.UnauthorizedResponse + JSON403 *externalRef0.ForbiddenResponse + JSON500 *externalRef0.InternalServerErrorResponse +} + +// Status returns HTTPResponse.Status +func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse struct { Body []byte HTTPResponse *http.Response @@ -657,6 +785,23 @@ func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjec return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesResponse(rsp) } +// PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse request with arbitrary body returning *PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse +func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBodyWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { + rsp, err := c.PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithBody(ctx, organizationID, projectID, regionID, identityID, contentType, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp) +} + +func (c *ClientWithResponses) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter, body PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody, reqEditors ...RequestEditorFn) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { + rsp, err := c.PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(ctx, organizationID, projectID, regionID, identityID, body, reqEditors...) + if err != nil { + return nil, err + } + return ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp) +} + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesWithResponse request returning *GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse func (c *ClientWithResponses) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesWithResponse(ctx context.Context, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, reqEditors ...RequestEditorFn) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse, error) { rsp, err := c.GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(ctx, organizationID, projectID, regionID, reqEditors...) @@ -861,6 +1006,60 @@ func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDId return response, nil } +// ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse parses an HTTP response from a PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksWithResponse call +func ParsePostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse(rsp *http.Response) (*PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 201: + var dest PhysicalNetworkResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON201 = &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 +} + // ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse parses an HTTP response from a GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesWithResponse call func ParseGetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse(rsp *http.Response) (*GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImagesResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/pkg/openapi/router.go b/pkg/openapi/router.go index c7dcf8c..7e4c3cb 100644 --- a/pkg/openapi/router.go +++ b/pkg/openapi/router.go @@ -27,6 +27,9 @@ type ServerInterface interface { // (POST /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/identities) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter) + // (POST /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/identities/{identityID}/physicalNetworks) + PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter) + // (GET /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/images) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter) } @@ -55,6 +58,11 @@ func (_ Unimplemented) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegi w.WriteHeader(http.StatusNotImplemented) } +// (POST /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/identities/{identityID}/physicalNetworks) +func (_ Unimplemented) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter, identityID IdentityIDParameter) { + w.WriteHeader(http.StatusNotImplemented) +} + // (GET /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/images) func (_ Unimplemented) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(w http.ResponseWriter, r *http.Request, organizationID OrganizationIDParameter, projectID ProjectIDParameter, regionID RegionIDParameter) { w.WriteHeader(http.StatusNotImplemented) @@ -244,6 +252,61 @@ func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsP handler.ServeHTTP(w, r.WithContext(ctx)) } +// PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks operation middleware +func (siw *ServerInterfaceWrapper) PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(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 "regionID" ------------- + var regionID RegionIDParameter + + err = runtime.BindStyledParameterWithLocation("simple", false, "regionID", runtime.ParamLocationPath, chi.URLParam(r, "regionID"), ®ionID) + if err != nil { + siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "regionID", 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.PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks(w, r, organizationID, projectID, regionID, identityID) + })) + + for _, middleware := range siw.HandlerMiddlewares { + handler = middleware(handler) + } + + handler.ServeHTTP(w, r.WithContext(ctx)) +} + // GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages operation middleware func (siw *ServerInterfaceWrapper) GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages(w http.ResponseWriter, r *http.Request) { ctx := r.Context() @@ -415,6 +478,9 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl r.Group(func(r chi.Router) { r.Post(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/identities", wrapper.PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities) }) + r.Group(func(r chi.Router) { + r.Post(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/identities/{identityID}/physicalNetworks", wrapper.PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks) + }) r.Group(func(r chi.Router) { r.Get(options.BaseURL+"/api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/images", wrapper.GetApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDImages) }) diff --git a/pkg/openapi/schema.go b/pkg/openapi/schema.go index 2f58c61..4437ff5 100644 --- a/pkg/openapi/schema.go +++ b/pkg/openapi/schema.go @@ -19,79 +19,87 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", - "AP//SBLGC8hFAAA=", + "H4sIAAAAAAAC/+x8+3PcNpL/v4Lid6uyW9/haN7SzC97ip04qiS2zpa9V+vxuUCyOUREAlwAHGmi0v9+", + "1QBfQ3IeGlvnTV1qN2UNiUejHx90Nxp8cHyRpIID18pZPDgplTQBDdL8YgFwzfTm6uV18RwfB6B8yVLN", + "BHcWzk0EpGiY/xEykH2n5zB8n1IdOT2H0wScRW1Ip+dI+FfGJATOQssMeo7yI0goTvEXCaGzcP7fWUXe", + "mX2rzm4zDyQHDeo1TaCi7PGx5wi5opz9TpG2vVRfclJvS65e7iB4e8S9ROtNij2UloyvDDmpFL+Brw/y", + "L29HcM4ddJRDPQvfJKwOcQzptM0OS7kY7hlofbRDgtLfi4DBlp6+tS/wkS+4Bm7+pGkaM98I8Ow3hWt5", + "cOCeJmkM+GcCmgZU0w4dIWuQnlBA6s97DgucheOfT2cXMArccE49dzIdB+6cjqk7HY7Pp+H5xWQ089pq", + "75rfjz1HpeAbnaEr5Sw+PhQN/ThTGqTLAqfnrGmc4cP5eDacDEa+G87nF+5k7vsu9UZDd+558zkN/TCA", + "C+fxE/LmOPYW5PxDMg2Wqc2l50wmoZCE8tLC+y2RoppHG8V8Gr8GfSfk7b+vGApCXW4pbYkjlRCye2fh", + "DAd987+zC6f3jYTU4OqxsiJFP5Ivsp9buEoFV9ZgPBrkQnqbP36asEBKIVGt+ZrGLPicE+D07JvP2wQW", + "5Hki2JC8i3M0G+xcHet+Wx82pCyGgNhOxExhqO8RIYk20GVbBwIU4UITXC1lfMlpHJctUK1JyCAOVB9J", + "hHsNkpcyUKew6+NDrqrjcD46H87cYRj47sQ799z5YAbuJITBcDoJQj8IK1UNhXAePx3NpAad3XoSM6WJ", + "CC17SNGn0BO74jCmayFPXWjdin0JpuENMwsazs8H7mDoDoY3g8HC/P+fhRXP6YU/G58P3MlgNnUnwYS6", + "84AO3PPZ+UUQTgZ+MA8q1qz6k37EVlECSZ8OB4P+cNUfDlZe3ZD9NPuRJizeOAvnimuIyX+B4OQ6pprx", + "LCEXw9nghvz13e0mprfwN6eHPZSzmPScgKlbZzEa9JxVmtn1Z7j6Yc9JIBFy4yyG81HPSUQAsbNwfhoO", + "BogDwANjFK8/XL28ukRiiubj0ePxoswFsF+CeSMrMSE9FgTAv8yWy2F2WHGmQBJfgtkIaKxIIIwdRXQN", + "2/aTSrZmMaxAfUUrv6OKBMAZBMTbEJrpSEimchvXEVMkoRviAfFppmwjJGqr4ZJrcQu8IJvx1Tbhyhcp", + "FDve5fVVCR5m7Ygc/LtqwUvOwQelqNzUlkwEN11SKdYsAEnSmOpQyMTIqvJUThFVy7og+B4V/DcR8X4g", + "4D+on0DfFwmq87b1jQajiTuYuuPhzXCyGA7r1kdnk3A+ms3d8QwG7mQ8HLneRTB0p6NgPg6ms7l3XttD", + "M478dRq+9hOsuPBksQuMZ/5gekHdC/CoOwmnnjsfhhM3nIWhN78Yn8+nvu2yZooJzvjqnaYaLbV6CEHd", + "8kUKXGnq3xouxSLDeQIIaRbjBmWevBA8ZCt8/ipK/c33+F909dPb2B//589NEr25P0dOnE9mk2A48cKL", + "c5gOQno+mo0vBrgiVA/Tlg7ns/MLOroYjmaT+Xng0dHEm078+YwOZpOQIp15lFCR+XS37S3QoBMfKj+N", + "FPu9VbuEruAZMH00GI3dwcgdjW6Go8VgshiOT9UqLxuNBhN3PeyPpv2Zu0ozdzqa9i+m/cHUPfchmAyn", + "k7qcV2n2UrK1DVWa4ItwLJXlSjmM6S1CfUclfLCvjTdUBRvOwskJQPYeDdmWvfsR27YhOqKaUAkEh6Ga", + "eTGQO6YjC2HbWMHt/vwO5BrkDwiQX4bwygz02f7sBvncjdKCWKT0Y8qSr4Dil5xkHO5T8DUExDQjwvcz", + "KSHYhm+61VJLyhUDrvM+lAdLji1V5vsAAaItJRK03PTJVWhHYgamEYR9qqBH0hioQphPhdSEaUKVCWmU", + "yqx5cKF/FBkPvoy9XOjPIQ6zg7c1LxQCtE+RSR8qhxTumdJfgdfvOUWt0oKEjAeGPXYqs9ZWrPbnRvRM", + "G1FXLHly9LcT8rvCvSKf82yIf27EN1hMpovJFMXXTq7dbxIhBWc+0QykOyY4oA+IacSj6KExTn5B/E6F", + "iPunxfPZrXtn486nCKZjBz4a6nPG7sf6vJGRRMZzF/R3+EJ8oT56m5+tE7wDY3AudADsaHlU/DXwu2vc", + "wju2hOU7RkQVgfsU/el+Td1VbSXNBM8r4CCZn0N8gi71CnqtHVLg4kZ9K+wUpM6zfztGvSQapIJ8VJuU", + "RcooD/Cv3E3/6ebmOm/iiwD6xOyzymzRVk3zhm+QBSOCOsTCnA894mV2N7fjQmApRfokA42RgTLKaAZX", + "JrS4vL5SROgIkHkUBxcKinFt4GLnwpUCzxJn8bEj0VLXq89+jHuk02vpSMZVluK2B9jXat9no/+9ckwT", + "9Ti9pnugIUmFpJLFm88Zp2vKYtxXah3LWYsHK0m5bsxqnhVT1rdIX/AwZj62T0BHIviMb2kci7sW6QkE", + "jBaDVIHqp14z595pFU3N+JCnE3NNy9OKXhEOmhGQ+e18fpX5/OjsdqMqsoSHG0xHIqnzQOJNAUit1Exb", + "6REsuzP0uVthDzRa7LHAubdncQixf/msTMgcsVzVZZ4FWoqdy1ZmW9CQqCfmv2pxFpWSbqrEVhch9k2b", + "x/VtcN/kaOLMf5vz79eiV23HOZzveYctmzwuCchH6uJ0rfsTllZLj3V0kmCyIAl5cf2ehKZdPfdOoL/q", + "E5P+IjxLPJA9QqUfMQ2+ziR0Kp5Nr3Upnh0CNeHF9XtV64wB0Aok9rY5ua7eNBEZN3oEaQQJSBoTbI3u", + "xavvu0fL83r7pLJKMyuSKou3f3bbyszKOqdtyNbwoxw8X+FuAe+1oDIbeKS15KbQYSRbQXV7ua+u35PA", + "vCcsJAzNNo5xo2/qVxGPH2TyB9vwsRawH5g4b9crYj1i5yp35sPQlRNXTdnF9kIB2t7K9fttL6DDumzC", + "eL+yv7p+r0i5rXYr6i7VQxoOKlyZoW4rjumPLztN9cnC28VfO39NyS1jdrD7Qzltt/ztsHWvqEz2XP76", + "stMZ2EqYdfChTNIWAt065zxtN8ijynfoVQXFpoAEPHVjKEg5eWvYGuBJq++Ru4jFNq1vvTziU27xJg9u", + "iBaE8dB4sbDkOHmP3AEJBP9OFyl1ZY2U8oBI0JnkhOki3QVVipKQm4jaKTDMWHLPpNxNiGh6aUEC0CAT", + "xgFJ86M28TYy0YJg6MQ4tCW4lRk+lvPolL2zUWJxELy/s6arX/JEjpXIMQHlDbZsyth0PyTYir6WhH9Y", + "g9zoCP1aar1L07CQKAcIjBjDjPs7gMymzzuBjCaAMGZVRGRWWOUP32TYuz2Bega+rZUYdc0mBDiGTMHW", + "cCRkcbd3UUvlNEe8zmtrqqIVgkGGTzUEJiBDohkPJVVaZrsdmCLJ3xz/vcLd0EqEhaeN3vQNDNu3OVUS", + "UF/sPuWw9QJdcUbtjMBEk6djXREzmLlOxTfT+YtBrhqlteTiCLEqbjOSsZktaxy7Yf9JJv/YRWBCVzvk", + "gG++VdhhJj+d62XvJyxsy7k84FrkDXec2OxlS7P9bqnsdaqfeGBzpOtttaHD864OoX6hHsQfbFVRRy2U", + "qd34OfPANCYxtiamCKlH9CZlPo3jjd0+Ucu3wvp8ObjRerDkjAdwDyVyo9ARfY3kqMaI2lk4//1x4M4v", + "3X9S9/dPf/37ovrlfu5/ehj0ZsPHWou//f0vXeC5q56vY4E/l01tJoL8miltTobytb98/a4o3rGJvHhD", + "YnEH0hz3ED+ikvpo5b3c21ZESBJt0gi46hGlqdTGJQGe5+to1QmbljEtD8y8miRCaTIb18ZGnsXAVzpC", + "biX0/hfzw1nMxj0nYbz4OexgRv28Yo+buHhwaBy/CU1K/hgUbjiZD033p3FM0rWrb9XE1nZMXU8QeRAL", + "vkLX4fBW1pi0jSWfus6kdnjrrZOOb+6jNyg/GU+7xunmQJ0B36lDkWhxBtUB0lfX6wmhQSBBKWLbdfo9", + "T90A60vPpz9ixbu8lZbQv7HT8tUEflADjzb/I+GkDQh7nOZ6UfrXgIF9Luun8rxyl+Hbt18u8VMs3M59", + "spxr3VsLu+LoOlispZ7IdM7f7sU+VzBZ67On5v87VUXcOEQ9DVPF1V05mOLEdI+rVZ6XHulE1bSlw5OS", + "TY+4IyDIVbiQYO5ZmMq/OCaX11eVmkuggU1h3CFgqLZk9h42bR2t1F7l+UthfhiXjWarBJdptMCE18Y5", + "SYRxPrmGe733SOe4+xQ177KpIvY0p8bB645D9R1IUbYzB54mM1AvPKmUJeO3XNzxxpF9/ac5/Qug8doe", + "t3Ur2Jeg584o6qEl5RiqQoguNmiWwDZE2qLVGLTNkVtzdxZOQDW42HxHLqOD68fgW4e8OoC42aQDkXtP", + "NBhjI/26MLY31z8t8IkWqCBZd4dHChLKNfOLc5BGsLZeLoP/v1z2a/90BmRdUXVjd8JIFR3D8pinmLL8", + "txBeWyD1ksoD9mdX2hmk77DNJwdGe6y6Vr3WpTem8PsuEiRvt2Xe3dnNrXqp42Ein+B4mNhVAZBx9q/s", + "UCFA12H+FuVHgMKNqdos7JeprXRDnmn4DWN3U9how+mt04Elp3yzvXlgmwhorKO8cMaW2HjAIWSahFIk", + "hOIrHlBT+rLkJQWWbf0ldzpUSdNVZxREpce0pHJDNF1Zm0caTD6lrdbd1ROXBc+LIbpP1bozOigy86pI", + "pmu6OiwwQ0gx5qfu9ZqIbI/XhYHd0S4X8q/laxmg8jPJ9OYdtsvzDKZOa7tirE3HmxSkdX3Ls6K8xMoD", + "KtHLNOVk2wVtxjpicWfmKeqfzJsXIoDWw/cydhZOpHWqFmdlvrmfcXYrJHdNer0v5OrMkny2Hp1t9UfH", + "HgMqnA4XjxSdMKbpt4Vw5pWts2M8FG3uvDCnHvl124ApX6xBbuxBmshM1lqBXLMchJiOcdxa6uyt7frO", + "NsL9dKsmftAf9ocmF5UCpylzFs64P+iP7WYSGf6e0ZSdrYdb8aU6e9i+EP1Y5FLU2UN5RfnxbKfT/zY/", + "PQwgZBwItec75gSwqgFfgVhJmkYm7jdV3xsSi5X5mVKpmdGFJf+BmTK6O7opiwHs1RuWMM3MLR2q8anJ", + "hmpBqFLCZ+iflqihMj8iVC351qSx8GkMiFl5SsxeIPpOIcCBXENAvFh4aEZoLJkGAtpHkqgfFXKLqCJM", + "KyLueBU8lSTkuUemewY484LP6piuZ4vdiwEUGKOtX8BSgpjTPZUndO39JbvQ6kxXxagA6I8vuYqoLM/U", + "dCRFtorIXUQ1rEGSBPwIl5ogy8rKBFsASXXeq1gI6t0KOgAGYSd3CsuoThTGbmqtX4G+TNmH4Zu6Yr3Z", + "vrmfK9V1oVK53jiNi6yjwWAXcpXtzprF0I89ZzIYHu7XWbr72HOmx0y67/ZGHTiNE9MNmR8/2exo7ZsM", + "OxyeqsnZri8goOtzoGvH1woMAV8BCM4eik8CPJa1enx3bWCMGF/VFZOl0yoNXDrGbAp1zP2NvGQUYSDp", + "L/k/TBXDi8vrN8ZEynqFVqUh2inEYY8wTXxJU0VEpom75FSRFCTJVEZj4hIW2mjAVO4KDtbtyXjQI3eS", + "+relVXNckXFr+kt+E2WK3AFRmsWxOXfHRUWUBzEU91GswdKYKC7uwpjewk4LewWa0HIb7yya/GoG9zYX", + "2w9NoZ1iiDtvU3+pRU4G48Od2/dkTc/J4Z6t+0H/x0DgcK/290OeAzl2lkK+yPdg26BwjdTePUqZTcrf", + "6pmjSHFxOJcDBDYOxP28xKTnMLIf8/WdYlvNe/tGt4/o1/EpiD/3xz+eaRQFSF01Db9STlcQVHU/pX0Q", + "clX2IyoSWWxOvBXjq9gkP2ypNy2e5B8+sZW/3AeztzFFYnS4FfFiqjSRNGCZKooKYA22QJrWfFcSA701", + "t+sZJ0ok9qYmOp2CBYp42Qr7L/l2YjlPWdmM1x9O5j0nFV0B+QuT+CGUcLirfUiLNw6DtsHmWqgvRptK", + "9E79m0qb3bZb++zSWfObS48t0DoCQVqfQ/iGqHW6D/G/infPCR1nD9Xn2R6bR907gUW1zugRWK6bz0wR", + "VfnD7LN5uF99eEfIJfeoBIzOY8JF0DhbytHowy+Xr/uEvBY6D/FNDVCJUmWsXTr3ipir5lzHm2V1rcx4", + "9lXJM0brFRoitSgzc6vPVBWjv409UgY2oG6Xdf7xYOlwp66PAB6JZk1BPDeOXVWkNnX3BITb8VWzk4Bu", + "1637P/HuG+DdjtLPIogw70+JIeoVo8eEEM8SqF/Z1Z0SQjS+EvNnBPHvHEE8Pv5PAAAA///hWnyKvFUA", + "AA==", } // 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 3016940..f6bdd5f 100644 --- a/pkg/openapi/server.spec.yaml +++ b/pkg/openapi/server.spec.yaml @@ -94,6 +94,33 @@ 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}/regions/{regionID}/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 + 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/regionIDParameter' + - $ref: '#/components/parameters/identityIDParameter' + post: + description: Create a new provider network. + security: + - oauth2Authentication: [] + requestBody: + $ref: '#/components/requestBodies/physicalNetworkRequest' + responses: + '201': + $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' /api/v1/organizations/{organizationID}/projects/{projectID}/regions/{regionID}/externalnetworks: description: |- Allows access to "external networks" for providers that support them. @@ -293,15 +320,41 @@ components: type: array items: $ref: '#/components/schemas/flavor' - identityWrite: - description: Request parameters for creating an identity. + tag: + description: An arbitrary tag name and value. type: object required: - - clusterId + - name + - value properties: - clusterId: - description: Cluster the owns the resource. + name: + description: A unique tag name. type: string + value: + description: The value of the tag. + type: string + tagList: + description: A list of tags. + type: array + items: + $ref: '#/components/schemas/tag' + identityWriteSpec: + description: Request parameters for creating an identity. + type: object + properties: + tags: + $ref: '#/components/schemas/tagList' + identityWrite: + description: An identity request. + type: object + required: + - metadata + - spec + properties: + metadata: + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/resourceWriteMetadata' + spec: + $ref: '#/components/schemas/identityWriteSpec' identitySpecOpenStack: description: Everything an OpenStack client needs to function. type: object @@ -332,6 +385,8 @@ components: required: - type properties: + tags: + $ref: '#/components/schemas/tagList' type: $ref: '#/components/schemas/regionType' openstack: @@ -347,6 +402,39 @@ components: $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/projectScopedResourceReadMetadata' spec: $ref: '#/components/schemas/identitySpec' + physicalNetworkSpec: + description: A phyical network's specification. + type: object + required: + - prefix + properties: + tags: + $ref: '#/components/schemas/tagList' + prefix: + description: An IPv4 address prefix. + type: string + physicalNetworkWrite: + description: A physical network request. + type: object + required: + - metadata + - spec + properties: + metadata: + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/resourceWriteMetadata' + spec: + $ref: '#/components/schemas/physicalNetworkSpec' + physicalNetworkRead: + description: A physical network. + type: object + required: + - metadata + - spec + properties: + metadata: + $ref: 'https://raw.githubusercontent.com/unikorn-cloud/core/main/pkg/openapi/common.spec.yaml#/components/schemas/projectScopedResourceReadMetadata' + spec: + $ref: '#/components/schemas/physicalNetworkSpec' externalNetwork: description: An Openstack external network. type: object @@ -374,7 +462,30 @@ components: schema: $ref: '#/components/schemas/identityWrite' example: - clusterId: 9361402c-f998-49cc-ab21-9bb99afcfde8 + metadata: + id: c7568e2d-f9ab-453d-9a3a-51375f78426b + name: identity-name + description: A verbose description + spec: + tags: + - name: cluster-id + value: 9361402c-f998-49cc-ab21-9bb99afcfde8 + physicalNetworkRequest: + description: A request for a physical network. + content: + application/json: + schema: + $ref: '#/components/schemas/physicalNetworkWrite' + example: + metadata: + id: c7568e2d-f9ab-453d-9a3a-51375f78426b + name: physical-network-name + description: A verbose description + spec: + tags: + - name: cluster-id + value: 9361402c-f998-49cc-ab21-9bb99afcfde8 + prefix: 10.0.0.0/8 responses: regionsResponse: description: A list of regions. @@ -436,12 +547,14 @@ components: schema: $ref: '#/components/schemas/identityRead' example: + # TODO: metadata for region? metadata: id: a64f9269-36e0-4312-b8d1-52d93d569b7b name: unused organizationId: 9a8c6370-4065-4d4a-9da0-7678df40cd9d projectId: e36c058a-8eba-4f5b-91f4-f6ffb983795c creationTime: 2024-05-31T14:11:00Z + createdBy: john.doe@acme.com provisioningStatus: provisioned spec: type: openstack @@ -450,6 +563,25 @@ components: cloudConfig: dGhpcyBpcyBhIHRlc3QK projectId: eb9c92d937464d14bf87e50fa726380d userId: a19678a28126497dba24b54c96a064fa + physicalNetworkResponse: + description: A physical network. + content: + application/json: + schema: + $ref: '#/components/schemas/physicalNetworkRead' + example: + # TODO: metadata for region? + # TODO: metadata for identity? + metadata: + id: a64f9269-36e0-4312-b8d1-52d93d569b7b + name: unused + organizationId: 9a8c6370-4065-4d4a-9da0-7678df40cd9d + projectId: e36c058a-8eba-4f5b-91f4-f6ffb983795c + creationTime: 2024-05-31T14:11:00Z + createdBy: john.doe@acme.com + provisioningStatus: provisioned + spec: + prefix: 10.0.0.0/8 externalNetworksResponse: description: A list of valid external networks. content: diff --git a/pkg/openapi/types.go b/pkg/openapi/types.go index 7aceb21..cbc53a8 100644 --- a/pkg/openapi/types.go +++ b/pkg/openapi/types.go @@ -110,6 +110,9 @@ type IdentitySpec struct { // Openstack Everything an OpenStack client needs to function. Openstack *IdentitySpecOpenStack `json:"openstack,omitempty"` + // Tags A list of tags. + Tags *TagList `json:"tags,omitempty"` + // Type The region's provider type. Type RegionType `json:"type"` } @@ -129,10 +132,19 @@ type IdentitySpecOpenStack struct { UserId string `json:"userId"` } -// IdentityWrite Request parameters for creating an identity. +// IdentityWrite An identity request. type IdentityWrite struct { - // ClusterId Cluster the owns the resource. - ClusterId string `json:"clusterId"` + // Metadata Resource metadata valid for all API resource reads and writes. + Metadata externalRef0.ResourceWriteMetadata `json:"metadata"` + + // Spec Request parameters for creating an identity. + Spec IdentityWriteSpec `json:"spec"` +} + +// IdentityWriteSpec Request parameters for creating an identity. +type IdentityWriteSpec struct { + // Tags A list of tags. + Tags *TagList `json:"tags,omitempty"` } // Image An image. @@ -161,6 +173,32 @@ type Images = []Image // 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 +// PhysicalNetworkRead A physical network. +type PhysicalNetworkRead struct { + Metadata externalRef0.ProjectScopedResourceReadMetadata `json:"metadata"` + + // Spec A phyical network's specification. + Spec PhysicalNetworkSpec `json:"spec"` +} + +// PhysicalNetworkSpec A phyical network's specification. +type PhysicalNetworkSpec struct { + // Prefix An IPv4 address prefix. + Prefix string `json:"prefix"` + + // Tags A list of tags. + Tags *TagList `json:"tags,omitempty"` +} + +// PhysicalNetworkWrite A physical network request. +type PhysicalNetworkWrite struct { + // Metadata Resource metadata valid for all API resource reads and writes. + Metadata externalRef0.ResourceWriteMetadata `json:"metadata"` + + // Spec A phyical network's specification. + Spec PhysicalNetworkSpec `json:"spec"` +} + // RegionRead A region. type RegionRead struct { // Metadata Resource metadata valid for all reads. @@ -188,6 +226,21 @@ type SoftwareVersions struct { Kubernetes *externalRef0.Semver `json:"kubernetes,omitempty"` } +// Tag An arbitrary tag name and value. +type Tag struct { + // Name A unique tag name. + Name string `json:"name"` + + // Value The value of the tag. + Value string `json:"value"` +} + +// TagList A list of tags. +type TagList = []Tag + +// IdentityIDParameter 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 IdentityIDParameter = KubernetesNameParameter + // OrganizationIDParameter defines model for organizationIDParameter. type OrganizationIDParameter = string @@ -209,11 +262,20 @@ type IdentityResponse = IdentityRead // ImagesResponse A list of images that are compatible with this platform. type ImagesResponse = Images +// PhysicalNetworkResponse A physical network. +type PhysicalNetworkResponse = PhysicalNetworkRead + // RegionsResponse A list of regions. type RegionsResponse = Regions -// IdentityRequest Request parameters for creating an identity. +// IdentityRequest An identity request. type IdentityRequest = IdentityWrite +// PhysicalNetworkRequest A physical network request. +type PhysicalNetworkRequest = PhysicalNetworkWrite + // PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesJSONRequestBody defines body for PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentities for application/json ContentType. type PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesJSONRequestBody = IdentityWrite + +// PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody defines body for PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworks for application/json ContentType. +type PostApiV1OrganizationsOrganizationIDProjectsProjectIDRegionsRegionIDIdentitiesIdentityIDPhysicalNetworksJSONRequestBody = PhysicalNetworkWrite diff --git a/pkg/providers/interfaces.go b/pkg/providers/interfaces.go index 1d8b07b..2766b86 100644 --- a/pkg/providers/interfaces.go +++ b/pkg/providers/interfaces.go @@ -20,6 +20,7 @@ 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. @@ -31,9 +32,11 @@ type Provider interface { // Images lists all available images. Images(ctx context.Context) (ImageList, error) // CreateIdentity creates a new identity for cloud infrastructure. - CreateIdentity(ctx context.Context, info *ClusterInfo) (*unikornv1.Identity, *CloudConfig, error) + CreateIdentity(ctx context.Context, organizationID, projectID string, request *openapi.IdentityWrite) (*unikornv1.Identity, *CloudConfig, error) // DeleteIdentity cleans up an identity for cloud infrastructure. DeleteIdentity(ctx context.Context, identityID string) error + // CreatePhysicalNetwork create a new physical network. + CreatePhysicalNetwork(ctx context.Context, organizationID, projectID, identityID string, request *openapi.PhysicalNetworkWrite) (*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 7d5be9e..0f1b7f2 100644 --- a/pkg/providers/openstack/network.go +++ b/pkg/providers/openstack/network.go @@ -137,14 +137,14 @@ func (c *NetworkClient) AllocateVLAN(ctx context.Context) (int, error) { } // CreateVLANProviderNetwork creates a VLAN provider network for a project. -func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name string, projectID string) (*networks.Network, error) { +func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name string, projectID string) (int, *networks.Network, error) { if c.options == nil || c.options.PhysicalNetwork == nil { - return nil, ErrConfiguration + return -1, nil, ErrConfiguration } vlanID, err := c.AllocateVLAN(ctx) if err != nil { - return nil, err + return -1, nil, err } tracer := otel.GetTracerProvider().Tracer(constants.Application) @@ -167,5 +167,10 @@ func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name stri }, } - return networks.Create(ctx, c.client, opts).Extract() + network, err := networks.Create(ctx, c.client, opts).Extract() + if err != nil { + return -1, nil, err + } + + return vlanID, network, nil } diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index a3bf080..5cae895 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -31,13 +31,14 @@ import ( "github.com/gophercloud/gophercloud/v2/openstack/identity/v3/users" "github.com/gophercloud/utils/openstack/clientconfig" - "github.com/unikorn-cloud/core/pkg/constants" + "github.com/unikorn-cloud/core/pkg/server/conversion" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" + "github.com/unikorn-cloud/region/pkg/constants" + "github.com/unikorn-cloud/region/pkg/openapi" "github.com/unikorn-cloud/region/pkg/providers" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apimachinery/pkg/util/uuid" @@ -326,26 +327,19 @@ func (p *Provider) Images(ctx context.Context) (providers.ImageList, error) { } const ( - // ProjectIDAnnotation records the project ID created for a cluster. - ProjectIDAnnotation = "openstack." + providers.MetdataDomain + "/project-id" - // UserIDAnnotation records the user ID create for a cluster. - UserIDAnnotation = "openstack." + providers.MetdataDomain + "/user-id" - // Projects are randomly named to avoid clashes, so we need to add some tags // in order to be able to reason about who they really belong to. It is also // useful to have these in place so we can spot orphaned resources and garbage // collect them. OrganizationTag = "organization" ProjectTag = "project" - ClusterTag = "cluster" ) // projectTags defines how to tag projects. -func projectTags(info *providers.ClusterInfo) []string { +func projectTags(organizationID, projectID string) []string { tags := []string{ - OrganizationTag + "=" + info.OrganizationID, - ProjectTag + "=" + info.ProjectID, - ClusterTag + "=" + info.ClusterID, + OrganizationTag + "=" + organizationID, + ProjectTag + "=" + projectID, } return tags @@ -368,10 +362,10 @@ func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityC // provisionProject creates a project per-cluster. Cluster API provider Openstack is // somewhat broken in that networks can alias and cause all kinds of disasters, so it's // safest to have one cluster in one project so it has its own namespace. -func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, info *providers.ClusterInfo) (*projects.Project, error) { +func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, organizationID, projectID string) (*projects.Project, error) { name := "unikorn-" + rand.String(8) - project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(info)) + project, err := identityService.CreateProject(ctx, p.domainID, name, projectTags(organizationID, projectID)) if err != nil { return nil, err } @@ -475,10 +469,33 @@ func (p *Provider) createClientConfig(applicationCredential *applicationcredenti return credentials, 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 +} + // CreateIdentity creates a new identity for cloud infrastructure. // //nolint:cyclop -func (p *Provider) CreateIdentity(ctx context.Context, info *providers.ClusterInfo) (*unikornv1.Identity, *providers.CloudConfig, error) { +func (p *Provider) CreateIdentity(ctx context.Context, organizationID, projectID string, request *openapi.IdentityWrite) (*unikornv1.Identity, *providers.CloudConfig, error) { identityService, err := p.identity(ctx) if err != nil { return nil, nil, err @@ -486,7 +503,7 @@ func (p *Provider) CreateIdentity(ctx context.Context, info *providers.ClusterIn // Every cluster has its own project to mitigate "nuances" in CAPO i.e. it's // totally broken when it comes to network aliasing. - project, err := p.provisionProject(ctx, identityService, info) + project, err := p.provisionProject(ctx, identityService, organizationID, projectID) if err != nil { return nil, nil, err } @@ -528,18 +545,15 @@ func (p *Provider) CreateIdentity(ctx context.Context, info *providers.ClusterIn }, } + objectMeta := conversion.NewObjectMetadata(&request.Metadata, p.region.Namespace) + objectMeta = objectMeta.WithOrganization(organizationID) + objectMeta = objectMeta.WithProject(projectID) + objectMeta = objectMeta.WithLabel(constants.RegionLabel, p.region.Name) + identity := &unikornv1.Identity{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: p.region.Namespace, - Name: string(uuid.NewUUID()), - Labels: map[string]string{ - constants.NameLabel: "undefined", - constants.OrganizationLabel: info.OrganizationID, - constants.ProjectLabel: info.ProjectID, - constants.KubernetesClusterLabel: info.ClusterID, - }, - }, + ObjectMeta: objectMeta.Get(ctx), Spec: unikornv1.IdentitySpec{ + Tags: convertTagList(request.Spec.Tags), Provider: unikornv1.ProviderOpenstack, OpenStack: &unikornv1.IdentitySpecOpenStack{ UserID: user.ID, @@ -583,6 +597,58 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identityID string) error return nil } +// GetIdentity looks up the specified identity resource. +func (p *Provider) GetIdentity(ctx context.Context, id string) (*unikornv1.Identity, error) { + out := &unikornv1.Identity{} + + if err := p.client.Get(ctx, client.ObjectKey{Namespace: p.region.Namespace, Name: id}, out); err != nil { + return nil, err + } + + return out, nil +} + +// CreatePhysicalNetwork creates a physical network for an identity. +func (p *Provider) CreatePhysicalNetwork(ctx context.Context, organizationID, projectID, identityID string, request *openapi.PhysicalNetworkWrite) (*unikornv1.PhysicalNetwork, error) { + identity, err := p.GetIdentity(ctx, identityID) + if err != nil { + return nil, err + } + + networkService, err := p.network(ctx) + if err != nil { + return nil, err + } + + vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "foo", identity.Spec.OpenStack.ProjectID) + if err != nil { + return nil, err + } + + objectMeta := conversion.NewObjectMetadata(&request.Metadata, p.region.Namespace) + objectMeta = objectMeta.WithOrganization(organizationID) + objectMeta = objectMeta.WithProject(projectID) + objectMeta = objectMeta.WithLabel(constants.RegionLabel, p.region.Name) + objectMeta = objectMeta.WithLabel(constants.IdentityLabel, identityID) + + physicalNetwork := &unikornv1.PhysicalNetwork{ + ObjectMeta: objectMeta.Get(ctx), + Spec: unikornv1.PhysicalNetworkSpec{ + Tags: convertTagList(request.Spec.Tags), + ProviderNetwork: &unikornv1.OpenstackProviderNetworkSpec{ + ID: providerNetwork.ID, + VlanID: vlanID, + }, + }, + } + + if err := p.client.Create(ctx, physicalNetwork); err != nil { + return nil, err + } + + return physicalNetwork, nil +} + // ListExternalNetworks returns a list of external networks if the platform // supports such a concept. func (p *Provider) ListExternalNetworks(ctx context.Context) (providers.ExternalNetworks, error) { diff --git a/pkg/providers/types.go b/pkg/providers/types.go index 9c7a586..e344a34 100644 --- a/pkg/providers/types.go +++ b/pkg/providers/types.go @@ -88,17 +88,6 @@ type Image struct { // ImageList allows us to attach sort functions and the like. type ImageList []Image -// ClusterInfo is required metadata when using the identity APIs to allow -// tracking of ownership information. -type ClusterInfo struct { - // OrganizationID defines which organization this belings to. - OrganizationID string - // ProjectID defines which project this belongs to. - ProjectID string - // ClusterID defines which cluster this belongs to. - ClusterID string -} - // ProviderType defines the provider to the client, while this is implicit, // as you had to select a region in the first instance, it's handy to refer to // to perform provider specific configuration. @@ -142,9 +131,9 @@ type CloudConfig struct { // ExternalNetwork represents an external network. type ExternalNetwork struct { - // ID is the provider specific netwokr ID. + // ID is the provider specific network ID. ID string - // Name is the netwokr name. + // Name is the network name. Name string }