From d6a402cb778b645c8efcc12e917b1a45a2167d83 Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Wed, 24 Jul 2024 09:44:55 +0100 Subject: [PATCH] External Network Selection and Image Meta In most cases I've ever seen, for various reasons, there are multiple external networks. We need to provide selection criteria to pick ones that actually work. Additionally, migrate to the new image metadata specification to enhance selection for a flavor. --- charts/region/Chart.yaml | 4 +- .../region.unikorn-cloud.org_regions.yaml | 127 ++++-- charts/region/templates/region.yaml | 58 +-- charts/region/values.schema.json | 414 ++++++++++++++++++ charts/region/values.yaml | 45 +- pkg/apis/unikorn/v1alpha1/types.go | 32 +- .../unikorn/v1alpha1/zz_generated.deepcopy.go | 126 +++++- pkg/handler/handler.go | 27 ++ pkg/openapi/schema.go | 172 ++++---- pkg/openapi/server.spec.yaml | 40 +- pkg/openapi/types.go | 40 +- pkg/providers/openstack/compute.go | 16 + pkg/providers/openstack/image.go | 40 +- pkg/providers/openstack/network.go | 63 ++- pkg/providers/openstack/network_test.go | 12 +- pkg/providers/openstack/provider.go | 42 +- pkg/providers/types.go | 22 + 17 files changed, 1006 insertions(+), 274 deletions(-) create mode 100644 charts/region/values.schema.json diff --git a/charts/region/Chart.yaml b/charts/region/Chart.yaml index 0bae27e..656d5b1 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.25 -appVersion: v0.1.25 +version: v0.1.26 +appVersion: v0.1.26 icon: https://raw.githubusercontent.com/unikorn-cloud/assets/main/images/logos/dark-on-light/icon.png diff --git a/charts/region/crds/region.unikorn-cloud.org_regions.yaml b/charts/region/crds/region.unikorn-cloud.org_regions.yaml index 12300af..233d359 100644 --- a/charts/region/crds/region.unikorn-cloud.org_regions.yaml +++ b/charts/region/crds/region.unikorn-cloud.org_regions.yaml @@ -190,60 +190,93 @@ spec: image: description: Image is configuration for the image service. properties: - propertiesInclude: + selector: description: |- - PropertiesInclude defines the set of properties that must all exist - for an image to be advertised by the provider. - items: - type: string - type: array - signingKey: - description: |- - SigningKey defines a PEM encoded public ECDSA signing key used to verify - the image is trusted. If specified, an image must contain the "digest" - property, the value of which must be a base64 encoded ECDSA signature of - the SHA256 hash of the image ID. - format: byte - type: string + Selector defines a set of rules to lookup images. + If not specified, all images are selected. + properties: + properties: + description: |- + Properties defines the set of properties an image needs to have to + be selected. + items: + type: string + type: array + signingKey: + description: |- + SigningKey defines a PEM encoded public ECDSA signing key used to verify + the image is trusted. If specified, an image must contain the "digest" + property, the value of which must be a base64 encoded ECDSA signature of + the SHA256 hash of the image ID. + format: byte + type: string + type: object type: object network: description: Network is configuration for the network service. properties: - physicalNetwork: - description: |- - PhysicalNetwork is the neutron provider specific network name used - to provision provider networks e.g. VLANs for bare metal clusters. - type: string - vlan: - description: |- - VLAN is the VLAN configuration. If not specified and a VLAN provider - network is requested then the ID will be allocated between 1-6094 - inclusive. + externalNetworks: + description: ExternalNetworks allows external network options + to be specified. properties: - segments: + selector: description: |- - Segements allow blocks of VLAN IDs to be allocated from. In a multi - tenant system, it's possible and perhaps necessary, that this controller - be limited to certain ranges to avoid split brain scenarios when another - user or system is allocating VLAN IDs for itself. - items: - properties: - endId: - description: EndID is the VLAN ID at the end of - the range. - maximum: 4094 - type: integer - startId: - description: StartID is VLAN ID at the start of - the range. - minimum: 1 - type: integer - required: - - endId - - startId - type: object - minItems: 1 - type: array + Selector defines a set of rules to lookup external networks. + In none is specified, all external networks are selected. + properties: + ids: + description: IDs is an explicit list of network IDs. + items: + type: string + type: array + tags: + description: Tags is an implicit selector of networks + with a set of all specified tags. + items: + type: string + type: array + type: object + type: object + providerNetworks: + description: ProviderNetworks allows provider networks to + be configured. + properties: + physicalNetwork: + description: |- + PhysicalNetwork is the neutron provider specific network name used + to provision provider networks e.g. VLANs for bare metal clusters. + type: string + vlan: + description: |- + VLAN is the VLAN configuration. If not specified and a VLAN provider + network is requested then the ID will be allocated between 1-6094 + inclusive. + properties: + segments: + description: |- + Segements allow blocks of VLAN IDs to be allocated from. In a multi + tenant system, it's possible and perhaps necessary, that this controller + be limited to certain ranges to avoid split brain scenarios when another + user or system is allocating VLAN IDs for itself. + items: + properties: + endId: + description: EndID is the VLAN ID at the end + of the range. + maximum: 4094 + type: integer + startId: + description: StartID is VLAN ID at the start + of the range. + minimum: 1 + type: integer + required: + - endId + - startId + type: object + minItems: 1 + type: array + type: object type: object type: object serviceAccountSecret: diff --git a/charts/region/templates/region.yaml b/charts/region/templates/region.yaml index 0dcdf63..465f115 100644 --- a/charts/region/templates/region.yaml +++ b/charts/region/templates/region.yaml @@ -21,59 +21,19 @@ spec: name: {{ $openstack.serviceAccountSecret.name }} {{- with $identity := $openstack.identity }} {{ printf "identity:" | nindent 4 }} - {{- with $roles := $identity.clusterRoles }} - {{ printf "clusterRoles:" | nindent 6 }} - {{- range $role := $roles }} - {{ printf "- %s" $role | nindent 6 }} - {{- end }} - {{- end }} + {{- toYaml $identity | nindent 6 }} {{- end }} {{- with $compute := $openstack.compute -}} - {{- printf "compute:" | nindent 4 }} - {{- with $policy := $compute.regionGroupPolicy -}} - {{ printf "regionGroupPolicy: %s" $policy | nindent 6 }} - {{- end }} - {{- with $flavors := $compute.flavors -}} - {{- printf "flavors:" | nindent 6 }} - {{- printf "selectionPolicy: %s" $flavors.selectionPolicy | nindent 8 }} - {{- with $includes := $flavors.include }} - {{- printf "include:" | nindent 8 }} - {{- range $include := $includes }} - {{- printf "- id: %s" $include.id | nindent 8 }} - {{- with $cpu := $include.cpu -}} - {{- printf "cpu:" | nindent 10 }} - {{- with $family := $cpu.family -}} - {{ printf "family: %s" $family | nindent 12 }} - {{- end }} - {{- end }} - {{- with $gpu := $include.gpu -}} - {{- printf "gpu:" | nindent 10 }} - {{- printf "vendor: %s" $gpu.vendor | nindent 12 }} - {{- printf "model: %s" $gpu.model | nindent 12 }} - {{- printf "memory: %s" $gpu.memory | nindent 12 }} - {{- printf "count: %v" $gpu.count | nindent 12 }} - {{- end }} - {{- end }} - {{- end }} - {{- with $excludes := $flavors.exclude -}} - {{- printf "exclude:" | nindent 8 }} - {{- range $exclude := $excludes }} - {{- printf "- id: %s" $exclude.id | nindent 8 }} - {{- end }} - {{- end }} - {{- end }} + {{ printf "compute:" | nindent 4 }} + {{- toYaml $compute | nindent 6 }} {{- end }} {{- with $image := $openstack.image -}} - {{- printf "image:" | nindent 4 }} - {{- with $properties := $image.propertiesInclude -}} - {{ printf "propertiesInclude:" | nindent 6 }} - {{- range $property := $properties }} - {{ printf "- %s" $property | nindent 6 }} - {{- end }} - {{- end }} - {{- with $signingKey := $image.signingKey -}} - {{ printf "signingKey: %s" $signingKey | nindent 6 }} - {{- end }} + {{ printf "image:" | nindent 4 }} + {{- toYaml $image | nindent 6 }} + {{- end }} + {{- with $network := $openstack.network -}} + {{ printf "network:" | nindent 4 }} + {{- toYaml $network | nindent 6 }} {{- end }} {{- end }} {{- end }} diff --git a/charts/region/values.schema.json b/charts/region/values.schema.json new file mode 100644 index 0000000..03bd173 --- /dev/null +++ b/charts/region/values.schema.json @@ -0,0 +1,414 @@ +{ + "$schema": "http://json-schema.org/schema#", + "type": "object", + "required": [ + "repository", + "organization", + "region", + "ingress", + "identity" + ], + "properties": { + "global": { + "type": "object", + "properties": { + "ca": { + "type": "object", + "properties": { + "secretNamespace": { + "type": "string" + }, + "secretName": { + "type": "string" + } + } + }, + "identity": { + "type": "object", + "properties": { + "host": { + "type": "string" + } + } + }, + "region": { + "type": "object", + "properties": { + "host": { + "type": "string" + } + } + }, + "kubernetes": { + "type": "object", + "properties": { + "host": { + "type": "string" + } + } + }, + "ui": { + "type": "object", + "properties": { + "host": { + "type": "string" + } + } + }, + "otlp": { + "type": "object", + "properties": { + "endpoint": { + "type": "string" + } + } + } + } + }, + "repository": { + "type": "string" + }, + "tag": { + "type": "string" + }, + "dockerConfig": { + "type": "string" + }, + "imagePullSecret": { + "type": "string" + }, + "image": { + "type": "string" + }, + "region": { + "type": "object", + "required": [ + "host" + ], + "properties": { + "host": { + "type": "string" + } + } + }, + "ingress": { + "type": "object", + "required": [ + "clusterIssuer" + ], + "properties": { + "class": { + "type": "string" + }, + "clusterIssuer": { + "type": "string" + }, + "externalDns": { + "type": "boolean" + } + } + }, + "cors": { + "type": "object", + "properties": { + "allowOrigin": { + "type": "array", + "items": { + "type": "string" + } + }, + "maxAge": { + "type": "integer" + } + } + }, + "identity": { + "type": "object", + "required": [ + "host" + ], + "properties": { + "host": { + "type": "string" + } + } + }, + "ca": { + "type": "object", + "reqired": [ + "secretNamespace", + "secretName" + ], + "properties": { + "secretNamespace": { + "type": "string" + }, + "secretName": { + "type": "string" + } + } + }, + "otlp": { + "type": "object", + "required": [ + "endpoint" + ], + "properties": { + "endpoint": { + "type": "string" + } + } + }, + "regions": { + "type": "array", + "items": { + "type": "object", + "required": [ + "name", + "provider" + ], + "properties": { + "name": { + "type": "string" + }, + "provider": { + "type": "string", + "enum": [ + "openstack" + ] + }, + "openstack": { + "type": "object", + "required": [ + "endpoint", + "serviceAccountSecret" + ], + "properties": { + "endpoint": { + "type": "string" + }, + "serviceAccountSecret": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "namespace": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "identity": { + "type": "object", + "properties": { + "clusterRoles": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "compute": { + "type": "object", + "properties": { + "flavors": { + "type": "object", + "required": [ + "selectionPolicy" + ], + "properties": { + "exclude": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + } + } + } + }, + "include": { + "type": "array", + "items": { + "type": "object", + "required": [ + "id" + ], + "properties": { + "baremetal": { + "type": "boolean" + }, + "cpu": { + "properties": { + "family": { + "type": "string" + } + }, + "type": "object" + }, + "gpu": { + "type": "object", + "required": [ + "count", + "memory", + "model", + "vendor" + ], + "properties": { + "count": { + "type": "integer" + }, + "memory": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "pattern": "^(\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\\+|-)?(([0-9]+(\\.[0-9]*)?)|(\\.[0-9]+))))?$" + }, + "model": { + "type": "string" + }, + "vendor": { + "type": "string", + "enum": [ + "NVIDIA", + "AMD" + ] + } + } + }, + "id": { + "type": "string" + } + } + } + }, + "selectionPolicy": { + "type": "string", + "enum": [ + "All", + "None" + ] + } + } + }, + "serverGroupPolicy": { + "type": "string" + } + } + }, + "image": { + "type": "object", + "properties": { + "selector": { + "type": "object", + "properties": { + "properties": { + "type": "array", + "items": { + "type": "string" + } + }, + "signingKey": { + "type": "string" + } + } + } + } + }, + "network": { + "type": "object", + "properties": { + "externalNetworks": { + "type": "object", + "properties": { + "selector": { + "type": "object", + "properties": { + "ids": { + "type": "array", + "items": { + "type": "string" + } + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + }, + "providerNetworks": { + "type": "object", + "properties": { + "physicalNetwork": { + "type": "string" + }, + "vlan": { + "type": "object", + "properties": { + "segments": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "endId", + "startId" + ], + "properties": { + "endId": { + "type": "integer", + "maximum": 4094 + }, + "startId": { + "type": "integer", + "minimum": 1 + } + } + } + } + } + } + } + } + } + } + } + } + }, + "oneOf": [ + { + "type": "object", + "required": [ + "openstack" + ], + "properties": { + "provider": { + "type": "string", + "enum": [ + "openstack" + ] + } + } + } + ] + } + } + } +} diff --git a/charts/region/values.yaml b/charts/region/values.yaml index cdb2d63..79c048a 100644 --- a/charts/region/values.yaml +++ b/charts/region/values.yaml @@ -12,12 +12,12 @@ organization: unikorn-cloud # Set the docker configuration, doing so will create a secret and link it # to the service accounts of all the controllers. You can do something like: # --set dockerConfig=$(cat ~/.docker/config.json | base64 -w0) -dockerConfig: +# dockerConfig: # Set the image pull secret on the service accounts of all the controllers. # This is an alternative to dockerConfigs, but unlikely to play ball with # ArgoCD as it's a foreign object that needs pruning. -imagePullSecret: +# imagePullSecret: # Region discovery information. # regions: @@ -75,18 +75,41 @@ imagePullSecret: # expression: ^(\d+)$ # # Image service configuration. # image: -# # Images must contain all the following properties to be exposed. -# propertiesInclude: -# - k8s -# # If specified the image signing key defines a base64 PEM encoded ECDSA -# # public key used to trust images. Images must have the "digest" property -# # defined, and its value must be the ECDSA signature of the SHA256 hash of -# # the image ID. -# signingKey: ~ +# # Image selection, the result is a boolean intersection of chosen options. +# selector: +# # Images must contain all the following properties to be exposed. +# properties: +# - k8s +# # If specified the image signing key defines a base64 PEM encoded ECDSA +# # public key used to trust images. Images must have the "digest" property +# # defined, and its value must be the ECDSA signature of the SHA256 hash of +# # the image ID. +# signingKey: ~ +# # Network service configuration. +# network: +# # External network selection, the result is a boolean intersection of +# # chosen options. +# externalNetworks: +# # Explicit list of network IDs. +# ids: +# - 49d51e7d-1c57-4480-9328-b466f9a12818 +# # Implicit tags a network must contain. +# tags: +# - unikorn:external-network +# # Provider network configuration. +# providerNetwoks: +# # Physical network to allocate network segements on. +# physicalNetwork: physnet1 +# # VLAN configuration. +# vlan: +# # A set of valid VLAN ID ranges for allocation +# segments: +# - startID: 1 +# endID: 4094 # REST server specific configuration. # Allows override of the global default image. -image: +# image: # Sets the DNS hosts/X.509 Certs. region: diff --git a/pkg/apis/unikorn/v1alpha1/types.go b/pkg/apis/unikorn/v1alpha1/types.go index 2d6d67a..bb26717 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -179,9 +179,15 @@ type GPUSpec struct { } type RegionOpenstackImageSpec struct { - // PropertiesInclude defines the set of properties that must all exist - // for an image to be advertised by the provider. - PropertiesInclude []string `json:"propertiesInclude,omitempty"` + // Selector defines a set of rules to lookup images. + // If not specified, all images are selected. + Selector *ImageSelector `json:"selector,omitempty"` +} + +type ImageSelector struct { + // Properties defines the set of properties an image needs to have to + // be selected. + Properties []string `json:"properties,omitempty"` // SigningKey defines a PEM encoded public ECDSA signing key used to verify // the image is trusted. If specified, an image must contain the "digest" // property, the value of which must be a base64 encoded ECDSA signature of @@ -190,6 +196,26 @@ type RegionOpenstackImageSpec struct { } type RegionOpenstackNetworkSpec struct { + // ExternalNetworks allows external network options to be specified. + ExternalNetworks *ExternalNetworks `json:"externalNetworks,omitempty"` + // ProviderNetworks allows provider networks to be configured. + ProviderNetworks *ProviderNetworks `json:"providerNetworks,omitempty"` +} + +type ExternalNetworks struct { + // Selector defines a set of rules to lookup external networks. + // In none is specified, all external networks are selected. + Selector *NetworkSelector `json:"selector,omitempty"` +} + +type NetworkSelector struct { + // IDs is an explicit list of network IDs. + IDs []string `json:"ids,omitempty"` + // Tags is an implicit selector of networks with a set of all specified tags. + Tags []string `json:"tags,omitempty"` +} + +type ProviderNetworks struct { // PhysicalNetwork is the neutron provider specific network name used // to provision provider networks e.g. VLANs for bare metal clusters. PhysicalNetwork *string `json:"physicalNetwork,omitempty"` diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index e9d0fce..1b2e301 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -47,6 +47,27 @@ func (in *CPUSpec) DeepCopy() *CPUSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ExternalNetworks) DeepCopyInto(out *ExternalNetworks) { + *out = *in + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(NetworkSelector) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExternalNetworks. +func (in *ExternalNetworks) DeepCopy() *ExternalNetworks { + if in == nil { + return nil + } + out := new(ExternalNetworks) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GPUSpec) DeepCopyInto(out *GPUSpec) { *out = *in @@ -187,6 +208,32 @@ func (in *IdentityStatus) DeepCopy() *IdentityStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ImageSelector) DeepCopyInto(out *ImageSelector) { + *out = *in + if in.Properties != nil { + in, out := &in.Properties, &out.Properties + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.SigningKey != nil { + in, out := &in.SigningKey, &out.SigningKey + *out = make([]byte, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ImageSelector. +func (in *ImageSelector) DeepCopy() *ImageSelector { + if in == nil { + return nil + } + out := new(ImageSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NamespacedObject) DeepCopyInto(out *NamespacedObject) { *out = *in @@ -203,6 +250,32 @@ func (in *NamespacedObject) DeepCopy() *NamespacedObject { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NetworkSelector) DeepCopyInto(out *NetworkSelector) { + *out = *in + if in.IDs != nil { + in, out := &in.IDs, &out.IDs + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.Tags != nil { + in, out := &in.Tags, &out.Tags + *out = make([]string, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NetworkSelector. +func (in *NetworkSelector) DeepCopy() *NetworkSelector { + if in == nil { + return nil + } + out := new(NetworkSelector) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenstackFlavorExclude) DeepCopyInto(out *OpenstackFlavorExclude) { *out = *in @@ -392,6 +465,32 @@ func (in *PhysicalNetworkStatus) DeepCopy() *PhysicalNetworkStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ProviderNetworks) DeepCopyInto(out *ProviderNetworks) { + *out = *in + if in.PhysicalNetwork != nil { + in, out := &in.PhysicalNetwork, &out.PhysicalNetwork + *out = new(string) + **out = **in + } + if in.VLAN != nil { + in, out := &in.VLAN, &out.VLAN + *out = new(VLANSpec) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ProviderNetworks. +func (in *ProviderNetworks) DeepCopy() *ProviderNetworks { + if in == nil { + return nil + } + out := new(ProviderNetworks) + 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 @@ -503,15 +602,10 @@ func (in *RegionOpenstackIdentitySpec) DeepCopy() *RegionOpenstackIdentitySpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegionOpenstackImageSpec) DeepCopyInto(out *RegionOpenstackImageSpec) { *out = *in - if in.PropertiesInclude != nil { - in, out := &in.PropertiesInclude, &out.PropertiesInclude - *out = make([]string, len(*in)) - copy(*out, *in) - } - if in.SigningKey != nil { - in, out := &in.SigningKey, &out.SigningKey - *out = make([]byte, len(*in)) - copy(*out, *in) + if in.Selector != nil { + in, out := &in.Selector, &out.Selector + *out = new(ImageSelector) + (*in).DeepCopyInto(*out) } return } @@ -529,14 +623,14 @@ func (in *RegionOpenstackImageSpec) DeepCopy() *RegionOpenstackImageSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RegionOpenstackNetworkSpec) DeepCopyInto(out *RegionOpenstackNetworkSpec) { *out = *in - if in.PhysicalNetwork != nil { - in, out := &in.PhysicalNetwork, &out.PhysicalNetwork - *out = new(string) - **out = **in + if in.ExternalNetworks != nil { + in, out := &in.ExternalNetworks, &out.ExternalNetworks + *out = new(ExternalNetworks) + (*in).DeepCopyInto(*out) } - if in.VLAN != nil { - in, out := &in.VLAN, &out.VLAN - *out = new(VLANSpec) + if in.ProviderNetworks != nil { + in, out := &in.ProviderNetworks, &out.ProviderNetworks + *out = new(ProviderNetworks) (*in).DeepCopyInto(*out) } return diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index eeaa36f..bc117de 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -177,6 +177,19 @@ func (h *Handler) GetApiV1OrganizationsOrganizationIDRegionsRegionIDFlavors(w ht util.WriteJSONResponse(w, r, http.StatusOK, out) } +func convertImageVirtualization(in providers.ImageVirtualization) openapi.ImageVirtualization { + switch in { + case providers.Virtualized: + return openapi.Virtualized + case providers.Baremetal: + return openapi.Baremetal + case providers.Any: + return openapi.Any + } + + return "" +} + func convertImage(in providers.Image) openapi.Image { out := openapi.Image{ Metadata: coreapi.StaticResourceMetadata{ @@ -185,6 +198,7 @@ func convertImage(in providers.Image) openapi.Image { CreationTime: in.Created, }, Spec: openapi.ImageSpec{ + Virtualization: convertImageVirtualization(in.Virtualization), SoftwareVersions: &openapi.SoftwareVersions{}, }, } @@ -193,6 +207,19 @@ func convertImage(in providers.Image) openapi.Image { out.Spec.SoftwareVersions.Kubernetes = coreutil.ToPointer(in.KubernetesVersion) } + if in.GPU != nil { + gpu := &openapi.ImageGpu{ + Vendor: convertGpuVendor(in.GPU.Vendor), + Driver: in.GPU.Driver, + } + + if len(in.GPU.Models) > 0 { + gpu.Models = &in.GPU.Models + } + + out.Spec.Gpu = gpu + } + return out } diff --git a/pkg/openapi/schema.go b/pkg/openapi/schema.go index f8bfbed..6b92250 100644 --- a/pkg/openapi/schema.go +++ b/pkg/openapi/schema.go @@ -19,91 +19,93 @@ import ( // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "H4sIAAAAAAAC/+w8a3PbRpJ/ZQq3VdmtIyi+JfLLnmInjiqOrbNl792aPtcAaBATATPYmQFlRqX/fjUP", - "vAGSouVk9861m7IIzKO7p9/dg3vHZ0nKKFApnNW9k2KOE5DA9S8SAJVE7q6eX+fP1eMAhM9JKgmjzsq5", - "iQDlA+0fIQE+dAYOUe9TLCNn4FCcgLOqLOkMHA7/yAiHwFlJnsHAEX4ECVZb/IlD6KycfzsrwTszb8XZ", - "beYBpyBBvMIJlJA9PAwcxjeYkt+wgm0v1JcUVceiq+c9ANdX3Au03KVqhpCc0I0GJ+XsV/DlQfrZcUjt", - "2QNHsdRXoRuHzSGKKTjNsMOnnC/3FWB9MEuCkN+zgECNT9+YF+qRz6gEqv/EaRoTXx/g2a9C4XLvwGec", - "pDGoPxOQOMASd/AI2gL3mABUfT5wSOCsHP98vriASeCGS+y5s/k0cJd4it35eHo+D88vZpOF12Z7V/9+", - "GDgiBV/taCn1iBUl3ghn9eE+X9qPMyGBuyRwBs4Wx5l6uJwuxrPRxHfD5fLCnS1938XeZOwuPW+5xKEf", - "BnDhPHxU1DzuQHIE/saJBHMMTWLZY0Eh4wjTQicMW0ygBCPaCeLj+BXIO8Zv/3kPLgfUpQbS1gGmHELy", - "2Vk549FQ/+/s4g87pAZVjz0rlM9DFsmh1QkiZVQYEcO+D6mE4I192KcfzLIRFsgDoCifhjAN0B2JY+QB", - "CrM4JHGsnood9SPOKMtEvBuu6X+zDCV4h1IWx0jqFQXLuA96gYRRIhlHRAokJJaZ0AgoSsSgwBiqk/Fw", - "YPmpCuzxfAWcM65klm5xTIJPFilnYN58qqOdo+yxYIfsFOfoEzN7dRzRm+qyISaKWmYS0lto6AeIcUsl", - "MzpgIBBlEilsMaFrigs6GglEIYE4EJpQ8FkCpwW7iFPI9eHeStU0XE7Oxwt3HAa+O/POPXc5WoA7C2E0", - "ns+C0A/CUqpCxpyHj0cTqQFnN0vHREjEQkMelM/JWdpgHMZ4y/ipiFYVjs9BD7whGqHx8nzkjsbuaHwz", - "Gq30//+eK5wlvvAX0/OROxst5u4smGF3GeCRe744vwjC2cgPlkFJms1wNozIJkogGeLxaDQcb4bj0car", - "6hw/zX7ECYl3zsq5ohJi9F/AKLqOsSQ0S9DFeDG6QX9+e7uL8S38xRmoGcJZzQZOQMSts5qMBs4mzQz+", - "mcJ+PHASSBjfOavxcjJwEhZA7Kycn8ajkVJZQAMtFK/eXz2/ulTA5MOnk4fjj9IewP4TtIPMiTHukSAA", - "+mWyXCzTI8WZAI58Dtpm4ViggGk5ivAW6vKTcrIlMWxAPKGU32GBAqAEAuTtEM5kxDgRVsZlRIRWih4g", - "H2fCDFJA1QauqWS3QHOwCd3UARc+SyE3zpfXV4Xy0LgrzUG/KxFeUwo+CIH5roIyYlRPSTnbkgA4SmMs", - "Q8YTfVbW4hN4MgGD4HvF47+yiA4DBv+B/QSGPksUR9cFcDKazNzR3J2Ob8az1XhcFUC8mIXLyWLpThcw", - "cmfT8cT1LoKxO58Ey2kwXyy984rFz6gisdOIJR4hyLmnrqbAdOGP5hfYvQAPu7Nw7rnLcThzw0UYesuL", - "6fly7pspWyIIo4Ru3mrDZjx+8xCCqvCzFKiQ2L/VVIpZpvYJIMRZrGyUfvKM0ZBs1PMXUervvlf/RVc/", - "vYn96X/+3ATRW/pLRYnz2WIWjGdeeHEO81GIzyeL6cVIYaQ4RI/F4+Xi/AJPLsaTxWx5Hnh4MvPmM3+5", - "wKPFLMROGUpoqC6W48ALR+4Ij8buDELfxaBcreD8PFwE09lkpj1aEzeViD1CoVR5Dgf79YodC6LKrbvT", - "FMs3Vv3GqoZVHxtB9fJpGTKh3PU2jJrgDXwFn2Uymkzd0cSdTG7Gk9VothpPT+VDL5tMRjN3Ox5O5sOF", - "u0kzdz6ZDy/mw9HcPfchmI3nsypnbNLsOSdbk2doOhfK3eDCUKVYRs9mobzDHN6b1zowKTMFzsqxACjy", - "Hq9BNHkPaA49BskIS4Q56IgDS+LFgO6IjIyJrttCavzPt8C3wH9QDsCXeTBCL/TJ/Ox2YmyYIBkynoAf", - "Y5I8gZdySVFG4XMKvork9DDEfD/jHIK6e4JrIyXHVBCg0s7BNFhTNVJkvg8QKG8CIw6S74boKjQrEe2G", - "KCfDxwIGKI0BC+XGpIxLRCTCQmcXhMiMeFAmf2QZDb6MvJTJT6Fapoe2lSgLgjIqLQIu+EyEfAJav6NY", - "cZVkKCQ00OQxW2lcW2mTb6brK5murrTOyYmYftekI/OSm6WvpvHP9fGNVrP5ajZXx9fOjH/eJYwzSnwk", - "CXB3itSCPiidhjysIhBC0Uulv1PG4uFpqbXs1r0zeZXHHMyXOIuWsPt1vR2kTyKjNsT6Db5Qv2BfRVOf", - "TJDXo2PUXsoBMKvZrM9T6O+udfPozwBmLUaEBYLPqYoXhxV2FxVMmmm/F0CBE9+q+ESFjBsYtCwkU8hN", - "huawU+DSpu57Vr1EErgAu6qpqCjIMA3UXzYM/enm5toO8VkAQ6TtrNAm2rCpHfhakWCCFA+R0NJhgLzM", - "WHOzLgQGUgUfJyBV5GuTjGpxk2q8vL4SiMkIFPGwWpwJyNc1gbnZS2EKNEuc1YeORGKVrz75sbKRzqDF", - "IxkVWarMHqi5hvs+af4fFGvqqN4ZNN0DCUnKOOYk3n3KKN5iEiu7UplY7Jo/2HBMZWNX/SzfsmoifUbD", - "mPhqfAIyYsEn9RbHMbtrgZ5AQHC+SJmI+ThoFsw6paLJGe9tZt9yms3we3m6Q68wdAYdxbiyCPHB6Xej", - "SrCYpwxMR6K0s5r4OldIrdRjm+mVsuxOn1u3wlQjW+QxinPvzLyCuB99UiQcj0BXdIlnri1ZL9pCmwUJ", - "iXhkfleBYGHCnONdmbjtAsS8adO4agb3ba5EnPhvLP1+yWdVLM7hfOZbNbJJ4wIAu1IXpSvTH4FaJf3b", - "MYmDzvIl6Nn1OxTqcdUyGILhZoh0ehfRLPGADxDmfkQk+DLj0Ml4Jn3cxXhmCcUJz67ficpkFQBtgKvZ", - "JufcNRsnLKOajyCNIAGOY6RGK/fixffdq9m89b5T2aSZOZIyS71/dzNK70o6t22craZHsbjFsP+A90pQ", - "ke0+UlqsKHQISS2obqP74vodCvR7REJElNjGsTL0Tf7K4/GDRH5vBj5UAvYDG9txgzzWQ2avwjIfVl0W", - "uHLLLrLnDND2Vq7f1b2ADukyBZH9zP7i+p1AhVntZtQ+1lMwHGS4ogLTZhw9X73sFNVHH14ffc3+FSY3", - "hOkh9/ti2+7zN8tWvaIi2XP5y/NOZ6CR2t0jQkU5Ij9aVM49Wq7q+bm2dNXedwDTC8TudONkg9y3yskL", - "chulAHisncpBOdlS1RZ4FPYDdBeR2FTRjNOJfEzN2dlYC0mGCA21Uw1rqjYfoDtAAaPfybyCJYzOwDRA", - "HGTGKSIyz75BmTFF6CbCZgsV9ayppytcOmLVsyRDAUjgCaGgQPOjNvAmUJIMqUiOUGifYC21fSzllY/4", - "1gSt1eTznk6rSh+N0paVmBgR2in/pvNkP0wSb17adJWZf0zYfKNGNlnHevUFLodYp6RAC+sftsB3MlKO", - "PDbutB6Y8wwFCDSjhBn1ezS3qTB0am6cgNIUhglZZtih+OHrIkS361MtUrT5XoWZixkCqmLEoLYcCknc", - "7U5VclfNFa9tJ2DZYodUVOVjCYGOQBXQhIYcC8mzfo8tr4M0138nlPm32jE8bfUGD9itqmjtYwPTldQV", - "QlXKHzpQPl1v5uGQ3utUXaknf7HCLFdpoZxX/8umW9POpJN2Rgz6Tcg/g/pokGW/FkjwpufY1Zs/KoDT", - "m59+yMXsRyBWc9MPOGl2YE/tay9ZmuMfHvoQ2BuePLL0dayzpbmhw8sqy3kvsQfxe9Mq2dHgqbu8fs48", - "0INRrEYj3Vk5QHKXEh/H8c5YfiVUtQSJRUf5CB6sKaEBfIbCJKhDV2pdnxyWErja8n8+jNzlpft37P72", - "8c9/XZW/3E/Dj/ejwWL8UBnxl7/+qUu6+tqaOxD8uRhqcjrol0xIXWOzuD9/9TZv8zMp0XiHYnYHXBfO", - "kB9hjn2lVAY2bhGIcRTt0gioGCAhMZfamwJqM5+4nKSGFtkBGuh9JUqYkGgxraytaBYD3chIUSvBn1/q", - "H85qMR04CaH5z3EHMaqVnz0e7urewXH8OtTFjWOUfsM/vm96bo2CU5f6rF0NqJjiWkeqBzGjG+WTHLaR", - "jU3buuRjV3WvJ9Bo1Yz+8PCiAfnJ+rRrnW4KVAnwnTgU0+fVvA4lfXW9nSEcBByEMpBq3JMbRrv9ERj3", - "OUetQ/+DfaQnO/CDHHi0+B+pTtoKYY83Xr2b8xRqYJ+H/LGICfsE37z98hM/RcLN3iefc2V6C7ErqlwH", - "o2uxxzJp6duN7FMErP0Q3tjV+zzq70SZLFBLVBNaZUqgK5uV1573uFpF5flIJ6rCLR2eFG96xB3xh2Xh", - "/AStZ6F7hOMYXV5flWzOAQcm+3LHTQ9l82T2lu1qRarKK5sJZvqHdtlwtkkUmpoLdNyunZOEaeeTSvgs", - "9xbHjrtWVvEumyxi6mIVCl53tCf0aIpinC4d65RDtYWnZJaM3lJ2RxvND9Wfuo4aQOO1KVx2M9iXaM/e", - "KOq+dcrmqo1pKekigyQJ1FWkaW+PQZpqgxF3Z+UEWIKrhvckSTqofox+6zivDkXcHNKhkQePFBgtI8Pq", - "YdSN6zcJfKQECki23eGRgARTSfy8otQI1rbrdfDv6/Ww8k9nQNYVVTesk4pUlWNYFMzyLYt/88NrH0i1", - "OfWA/BlMO4P0Htl8dGC0R6orfYBdfKOviNxFDNlxNfHuTpvWOs+OVxN2g+PVRF8vRUbJP7LDLRUJC3Tn", - "zkHMszQ4DvN8xQOY4zredvlj8e7q56iR/AhtdqMbd3PFQ0QtT2JTJL9mwva2mjxArSKzppju6lZPjYkA", - "xzKyvVOmy8oDCiGRKOQsQVi9ogHW3U9rWkBg8B6uqdMhAxJvOsM3zD0iOeY7JPHGKCsFg04EteWxu4Hm", - "MmeWfInuwmp3KkodqH6Vlxck3hwOBDQg+Zofu/HVoeQed1FFpEf7iop+LSdRa1g/40Tu3qpxNkGiW/Xq", - "TYNtOF6nwI3PXtTnbJedB5gr91h3FNZ7GjV7x+zOXCm2LXD6zTMWQOvhOx47KyeSMhWrsyIvP8wouWWc", - "urrYMmR8c2ZAPttOzmrzVUSiIkG1nUJeQXTCmnpeTTXrV6bVktCQtanzTNeBbBY+IMJnW+A7U7xkmc7u", - "C+BbYnUIkbFat5Lze2OmvjWDlCNQuxYxGo6HY51ES4HilDgrZzocDafGCkaavmc4JWfbcS0wFmf39Q9a", - "PFRuULXR+AVTvIGgLCJYoMUQoatiHhIRy2KdJBSEbmKtNU2fEc6f2Avwpu2E+jBcU61/YpIQKZAXYyER", - "xwHJRJ6HhS2Y7hxcuZmJYsC3+uoioUiwxFwTEAhvGQkE8rKNmr+mdV/cWnlF6w3IriZWqf2t4oaYuS2p", - "71Lg+pdC1Bos533dff4C5GVK3o9fV+n8ukblklZO4179ZDTqE91i3FnHxcqHgTM7ZmrHZXg9dXx4amfz", - "s548PTy5fW/3YeDMj0J2z82ZqsbSbk+3rvrw0eRTKx+z6XGRyiFnfZ+O0UsdKUs2vyPO7ovPtfy/E7An", - "ovrg4NSOj+soJydlXWbzmfYrEUYU7io1UdrINdUl+5qJg6JtC/biOoemIev5p2p2/Zxf+ZrNWfNTNg8t", - "fTE+Wl/svmmLo7XFk8n42X35iauHImHS4Tg+189rJXrlHiiXunTCsRDMJzrw0DE4kW0uNQt9AZ9e1b/J", - "VeO2yeEjaH0a5l+S22aj2eGZrbuGv79R+2Y/vqb9ODyr65t4T+gi1NRHs8zW60CIVn1QMcB185lu4Ch+", - "6ADfXtwtPw/E+Jp6mIMKy2NEWdDIa1uuef/y8tUQoVdMgllI9x8U3FQUSfJyJRFIXximMt6ty8tBKC3b", - "0XYDhEWlc1dDqwRH383SzZgq8lUzUgK+DrjbvWr/kuxzlNfSpOmT+yulHbhu8t0JnkzPJ95Ocmj67j1/", - "82uezq/pLVC+sU3aAYSEKmY0ORKEbio3/zfANhynkdY3+q7/DsVso3+mmCsGY3S4pj8QfXnyDu+KKyDm", - "g0LKzJCtVSZEmM4tFX7nHlCZqhSZHyEs1rS2acx8HMOgjNfNZ5G+E8qdUlQMkBczT2kNRfFMAgLpK5Cw", - "H+WpmkhpICkQu6OlvLWdsIHOldprvmWv8sB84iBfQIC2jtXPSgmGdIuzsM1n1TxD2TovYmLUG15TEWFe", - "NBbLiLNsE6G7CEvYAkcJ+JFCNVEkK+6jmGuvWNpZOSK96Y+XSq+aAlZRgX50jsOyyUkJjuaN9y8Vzv/z", - "iQZLsLP7/EurD8UtStp/azOO2Z0ob3yjtdO6tLl2NGvnLGO9BGuvlagmwzX9m77Q8ezy+rVm4+LqRusO", - "qJIliMMBIhL5HKcCsUwid02x0HY8ExmOkYtIaKqL+k41o2CqERkNBuiOY/+2kDyqMNK+iPZPM4HuAAlJ", - "4lhfEFBIRZgGMeRfCjFChWMkKLsLY3x7IAlYZNc7r7OeKhRv7Cn90DyjU4Sl97ON34Kn30lQD7t57e8p", - "f6F0914kfWZtmRlQxn/7dL3Qyt6vzbSSnn9W0BIPAlMBVXax0BtPIAg/WnRO4f/mRzz/QCfwG/seyb59", - "9wxy7tXvT2He6vWEY3j3KbT4lUHmpEpO/Vtu31j392Hdh4f/DQAA//9kZKLg6WAAAA==", + "H4sIAAAAAAAC/+w8a3PbyJF/ZQqXqk3qCIpvifyS09obrypeW2fLzl2WPtcA0yBmBcwgMwPKXJX++9U8", + "AAIEQFKUvJvcuZIti8A8unv63T2490KeZpwBU9Jb3HsZFjgFBcL8ogSYompz9fK6eK4fE5ChoJminHkL", + "7yYGVAx0f0QURN/reVS/z7CKvZ7HcAreorKk1/ME/COnAoi3UCKHnifDGFKst/iDgMhbeP92tgXvzL6V", + "Z7d5AIKBAvkGp7CF7OGh53Gxwoz+ijVse6G+ZKg6Fl297AC4vuJeoNUm0zOkEpStDDiZ4L9AqA7Sz41D", + "es8OOMqlvgrdBKwOUUzDaYcdPuViua8A64NdEqT6nhMKNT59Z1/oRyFnCpj5E2dZQkNzgGe/SI3LvQdf", + "cJoloP9MQWGCFW7hEbQGEXAJqPq851HiLbzwfDq7gBHxozkO/Ml0TPw5HmN/OhyfT6Pzi8loFjTZ3je/", + "H3qezCDUOzpKPWJFhVfSW/x8XywdJrlUIHxKvJ63xkmuH87Hs+FkMAr9aD6/8CfzMPRxMBr68yCYz3EU", + "RgQuvIdPmprHHUiBwN8EVWCPYZdY7lhQxAXCrNQJ/QYTaMGIN5KGOHkD6o6L23/egysA9ZmFtHGAmYCI", + "fvEW3nDQN/87u/jdDmmHqseeFSrmIYdk3+kEmXEmrYjhMIRMAXnnHnbpB7tsjCUKABgqpiHMCLqjSYIC", + "QFGeRDRJ9FO5YWEsOOO5TDb9JftvnqMUb1DGkwQps6LkuQjBLJByRhUXiCqJpMIqlwYBTYkENBh9fTIB", + "Jo6fqsAez1cgBBdaZtkaJ5R8dkh5Pfvmcx3tAuWAkw1yU7yjT8zu1XJE76rLRphqatlJyGxhoO8hLhyV", + "7GjCQSLGFdLYYsqWDJd0tBKIIgoJkYZQ8EWBYCW7yFPI9fO9k6pxNB+dD2f+MCKhPwnOA38+mIE/iWAw", + "nE5IFJJoK1UR597Dp6OJtANnO0snVCrEI0seVMwpWNpiHCV4zcWpiFYVTijADLyhBqHh/HzgD4b+YHgz", + "GCzM//9eKJw5vghn4/OBPxnMpv6ETLA/J3jgn8/OL0g0GYRkTrakWfUn/Ziu4hTSPh4OBv3hqj8crIKq", + "zgmz/C84pcnGW3hXTEGC/gs4Q9cJVpTlKboYzgY36I/vbzcJvoU/eT09Q3qLSc8jVN56i9Gg562y3OKf", + "a+yHPS+FlIuNtxjORz0v5QQSb+H9OBwMtMoCRoxQvPl49fLqUgNTDB+PHo4/SncA+0/QDbInxkVACQH2", + "NFkul+mQ4lyCQKEAY7NwIhHhRo5ivIa6/GSCrmkCK5DPKOV3WCICjAJBwQbhXMVcUOlkXMVUGqUYAApx", + "Lu0gDVRt4JIpfgusAJuyVR1wGfIMCuN8eX1VKg+Du9Yc7LstwkvGIAQpsdhUUEacmSmZ4GtKQKAswSri", + "IjVn5Sw+hWcTMCDfax7/hcesTzj8Bw5T6Ic81RxdF8DRYDTxB1N/PLwZThbDYVUA8WwSzUezuT+ewcCf", + "jIcjP7ggQ386IvMxmc7mwXnF4udMk9jbiSUeIciFp66nwHgWDqYX2L+AAPuTaBr482E08aNZFAXzi/H5", + "fBraKWsqKWeUrd4bw2Y9fvsQSFX4eQZMKhzeGiolPNf7EIhwnmgbZZ684CyiK/38VZyFm+/1f/HVj++S", + "cPyff90FMZiHc02J88lsQoaTILo4h+kgwuej2fhioDHSHGLG4uF8dn6BRxfD0WwyPycBHk2C6SScz/Bg", + "Nomwtw0lDFQX8yEJooE/wIOhP4Eo9DFoV4ucn0czMp6MJsajtXHTFrFHKJQqz2GyX6+4sSCr3Lo5TbF8", + "Y9VvrGpZ9bERVCefbkMmVLjellFTvIKv4LOMBqOxPxj5o9HNcLQYTBbD8al8GOSj0WDir4f90bQ/81dZ", + "7k9H0/7FtD+Y+uchkMlwOqlyhnM+iKBr0Pa5HO0510NHT96ldT6cD/LjaDDwPrX6IpJH6g4L+AhCc6GJ", + "WLYpBG/hOcj02DUVKseJkxb9rnigmfcRmsccywGNY8YgFWOFsAATqWBFgwTQHVWxNe11G8qs3/oexBrE", + "D9pxeJrnI81Cn+3PdufHhReKI+tBhAmm6TN4N5cM5Qy+ZBDqCNAMQzwMcyGA1N0aXBupBGaSAlNuDmZk", + "yfRImYchANFeCEYClNj00VVkV6LGfdHOSYgl9FCWAJba/cm4UIgqhKXJSkiZW7FiXP2F54w8jbyMq8+R", + "XqaDtpXoDMg2mi0DNfhCpXoGWn9gWHOV4iiijBjy2K0Mro10yzeT95VMXls66OQETrdL05KxKczZV7MU", + "5+b4BovJdDGZ6uNrZtS/bFIuOKMhUhSEP0Z6wRC0TkMB1pELZei11vkZ50n/tJRcfuvf2XzMYw7mKU6m", + "I+x+Xe8GmZPImQvNfoUn6hcc6ijssw0OO3SM3ks7DnY1ly16Dv3dtm4RNVrAnMWIsUTwJdNxZr/C7rKC", + "yW668BUwEDR0Kj7VoeYKeg0LyTVyo7497AyEcin/jlUvkQIhwa1qKzEaMsyI/suFrz/e3Fy7ISEn0EfG", + "zkpjoi2buoFvNQlGSPMQjRwdeijIrTW36wKxkGr4BAWlI2aXnNSL2xTl5fWVRFzFoImH9eJcQrGuDejt", + "XhpTYHmq3Z9mArLKV5/DRNtIr9fgkZzJPNNmD/Rcy32fDf/3yjVNNsDr7boHCtKMCyxosvmcM7zGNNF2", + "pTKx3LV4sBKYqZ1dzbNiy6qJDDmLEhrq8SmomJPP+i1OEn7XAD0FQnGxyDaB86m3W2hrlYpdzvjoKgKO", + "01xlICjSJGaFvtdrKeJtixc/e91u1BYsHmgD05Jgba1Cvi0UUiNl2WR6rSzb0+7OrbBVzAZ5rOLcO7Oo", + "PO5Hn5aJyiPQlW3iWWhL3om2NGZBQSofmRfWIDiYsBB4s034tgFi3zRpXDWD+zbXIk7Dd45+PxWzKhbn", + "cB70vR65S+MSALdSG6Ur0x+BWiVt3DJJgMkOpujF9QcUmXHV8hmC/qqPTGyGWJ4GIHoIizCmCkKVC2hl", + "PJt2bmM8u4TmhBfXH2Rlsg6AViD0bJurbpuNU54zw0eQxZCCwAnSo7V78er79tVcyLnvVFZZbo9km93e", + "v7sdZXalrdvunK2hR7m4w7D7gPdKUJklP1JanCi0CMkqy3+y6f7mbq+uP9QOvfWYiwVeU1vD7QJ5d7Hj", + "gS9BbAe/XRL0djXT3SIStvqxn0NfXX+QqLSF7dzVxS8G5UNcUpZb9tC/lfBFKuQg8T7agbv86OYX+1c4", + "0xKmjTW3q7WSTQNsl626Mi5V0/Muf3rZasF38rh7mKisPRRHi7Zzj+anejKuyVO19y3AdAKxOd2iuMj0", + "vfbMSGFYNACPNS4FKCebl9oCj8K+h+5imtiSmfUUUYiZPTsXICHFEWWR8YRhyfTmPXQHiHD2nSrKVdIm", + "dTAjSIDKBUNUFSkz2KZHEbqJsd1ChypLFphylgkzzSzFEQEFIqUMNGhh3ATeRjeKIx1+UQbNE6zlsY+l", + "vHbs3ttIs5pp3tNWVWmaQVSiSiCLKGuVf9tmsh8mhVevXY7Jzj8m1r3RI3dZx7niJS6HWGdLgQbWP6xB", + "bFSsvW9sfWAzsOAZBkAMo0Q5Czs0ty0ntGpunILWFJYJeW7ZofwRmopDu79SrUg0+V7HhrMJAqYDO1Jb", + "DkU0afeBKgmn3RWvXdvftp8O6VAoxAqICRs10JRFAksl8m43qyh67K7/QYIotWN02uo7POC2qqK1jw1s", + "C1Jb3FOpdZjo9nS9WcQwZq9TdaWZ/GSFuV2lgXJR6t922NreJZNps2LQbUL+GdTHDln2a4EUrzqOXb/5", + "vaIus/nph6xnv3KFq1a/x5azEI0Q1fYiSYA0US1qXgcWWdtSVq+objinqrRarQdalM6Oc6YLs/BsXqRD", + "rZN4Hf5EJ1ccEbKVh9JRBdzLTLvjW6qDR+z+sT6lQZ36607ifGzsXCfT37Sno6eawBOHsXZpjArXszWH", + "RHWfu1rX7HkBFqC5XHv5mG3anXBb0tzjfD+yoHmsN27URYsbvq3hvsYBJB9t42xLu6/p+ftrHoAZjBI9", + "Gpk+256mGQ1xkmysa6i1bi3t5dDRTmQAS0YZgS9Q+gxaK2i7b7gTKwVCb/k/Pw/8+aX/d+z/+umPf15s", + "f/mf+5/uB73Z8KEy4k9//kObtHY1ubcg+NdyqM3UoZ9yqUzl1OH+8s37ounTJrqTDUr4HQhTDkVhjAUO", + "tdXpFbE34gLFmywGJntIKiyUcbeBuXw23k7SQ8ucDyNmX4VSLhWajStra5olwFYq1tRK8ZfX5oe3mI17", + "XkpZ8XPYQoxqPW9PCLS493CSvI1MyeoYr2AngLrfde13yohtWrl2UaTiq9X6kwNIOFtpp/WwE7WzaVMl", + "fGqr2XZEoo1K4O8ef+5AfrLBbVunnQJVAnwnDyV9ihptiyG6ul5PECZEgNQelB737J6T2/4IjLu858ah", + "/85O9LMd+EEOPFr8j1QnTYWwJ1yr3tR6DjWwL4T6VCYNugTfvn36iZ8i4Xbvk8+5Mr2B2BXTroPVtTjg", + "uXL0bUf2OTIa3RDeuNW7Qq7v5DabpJeoel/bnFGbp1V0FOxxtcp+giOdqAq3tHhSYjdkaglQHQsXJ+g8", + "C9MxniTo8vpqy+YCMLHpuTthO2obYc6+Ymyt9Fh55aIdbn4Ylw3nq1SjabjAJHaMc5Jy43wyBV/U3pLn", + "cZcMK97lLovYameFgtctTScdmqIcZxoCjONebczaMkvObhm/YzstLdWfxocnsPPalqPbGewp2rMzzL5v", + "nLK9eGUbhdrIoGgKdRVpLzskoGx0bMXdW3gEK/D18I4sWgvVj9FvLefVooh3h7Ro5N4jBcbISL96GHXj", + "+k0CHymBEtJ1e3gkIcVM0bDImuwEa+vlkvz7ctmv/NMakLUlEHask4m1MwFlgqfYsvy3OLzmgVQ7kg/I", + "n8X04aHFNHXI5qMDoz1SXenubOMbc2HoLubIjauJd3tevdZPeLyacBscrya6OmRyRv+RH26USTkx/VgH", + "Mc8zchzmxYoHMMd1vN3yx+Ld1qVTI/kR2uzGtGMXisdmk0qgXIrkl1y6jmWbB6iV7JYMs03d6ukxMeBE", + "xa4jzvbOBcAgogpFgqcI61eMYNPTtmQlBBbv/pJ5LTKg8Ko1fMMioEpgsUEKr6yy0jCYRFBTHtvboi4L", + "ZimWaK+8t6ei9IGaV0X9SeHV4UDAAFKs+akd30O9FToiPdpX1PRrOIlGw4a5oGrzXo9zCRLTgFlvBW3C", + "8TYDYX32soDreicDwEK7x6ZPtN6patg74Xf2grlrbDRvXnACjYcfROItvFipTC7OysJNP2f0lgvmm2pc", + "n4vVmQX5bD06q83XEYmOBPV2GnkN0Qlrmnk11Wxe2QZayiLepM4LUyh0ZRpCZcjXIDa2us1zU/6RINbU", + "6RCqEr1uJef3zk59bwdpR8DcyTUGx1t4g/6wPzRJtAwYzqi38Mb9QX9srWBs6HuGM3q2HtYCY3l2X/+8", + "yUPlPl0TjZ8wwysg2yqTA1r2Eboq5yEZ8zwxSUJJ2SoxWtN2j+Hiifscgi2TsBD6S2b0T0JTqiQKEiwV", + "EpjQXBZ5WFiD7bnClXu6KAF8ay6yUoYkT+3lD4nwmlMiUZCv9Pwlq/vizsprWq9AtbUmK+NvlfcF7d1Z", + "c0MG178bo9fgBe+bOwWvQF1m9OPwbZXOb2tU3tLK2/nKwmgw6BLdctxZyzXbh543OWZqy6cRzNTh4amt", + "Le1m8vjw5OYt7oeeNz0K2T33oaoay7g97brq5082n1r5tFGHi7Qdctb1ISGz1JGy5PI78uy+/HjP/zsB", + "eyaq9w5ObfnUknZyMt5mNl8YvxJhxOCuUjRnO7mmumRfc3lQtF1Hh7wuoNmR9eLDRZtuzq982+hs98NG", + "Dw19MTxaX2y+aYujtcWzyfjZ/faDZw9lwqTFcXxpntd6OLR7oF3qrROOpeQhNYGHicGpanKpXegJfHpV", + "/0JbjdtGh4+g8aGgf0lumwwmh2c2bpD+9kbtm/34mvbj8Ky2LyQ+o4tQUx+7ZbZOB0I26oOaAa53n5kG", + "jvKHCfDddeztx6K4WLKycwQxTnby2o5rPr6+fNNH6A1XYBcy/QclN5VFkqJcSSUy18CZSjbL7ZUvlG37", + "FTc9hGWltdtAqwXH3Lgz3bo68tUzMgqhCbibzYz/kuxzlNeyS9Nn91e2duB6l+9O8GQ6Pvh3kkPTdZv9", + "m1/zfH5NZ4HyneviJxBRppnR5kgQuql8z2EFfCVwFht9Y77gsEEJX5mfGRaawTjrL9kP1FyJvcObss3R", + "fl5Kmxm6dsqEStu5pcPvwgPapiplHsYIyyWrbZrwECfQ28br9iNZ30ntTmkqEhQkPNBaQ1M8V4BAhRok", + "HMZFqibWGkhJxO/YVt6aTljP5Erd5e1tM3vPfriiWECCsY7Vj4xJjkwPvHTNZ9U8w/ZuhUyoVW94yWSM", + "Rdl5rmLB81WM7mKsYA0CpRDGGtVUk6y8sGQvM2PlZhWIdKY/Xmu9agtYZQX60TkOxyYnJTh2v2PwVOH8", + "P59ocAQ7uy++u/tQ3o1l3Xdxk4Tfye09frT0Gldxl55h7YJlnJfg7LUW1bS/ZH8zN35eXF6/NWxc3u1p", + "3OzVsgRJ1ENUoVDgTCKeK+QvGZbGjucyxwnyEY1sddHclOfM9bbmjPTQncDhbSl5TGNkfBHjn+YS3QGS", + "iiaJuUGikYoxIwkU33+xQoUTJBm/ixJ8eyAJWGbXWy8pnyoU79wp/bB7RqcIS+dHPL8FT7+RoB5285pf", + "136idHdeD37hbJkdsI3/9ul6aZR9WJvpJL34yKQjHhBbAdV2sdQbzyAIf3HonML/u590/R2dwG/seyT7", + "dt0zKLjX3mg4gXmr1xOO4d3n0OJXFpmTKjn1L/t9Y93fhnUfHv43AAD//x5uZRf3YgAA", } // 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 a21dd6d..46c238f 100644 --- a/pkg/openapi/server.spec.yaml +++ b/pkg/openapi/server.spec.yaml @@ -254,6 +254,13 @@ components: type: array items: $ref: '#/components/schemas/regionRead' + imageVirtualization: + description: What type of machine the image is for. + type: string + enum: + - virtualized + - baremetal + - any softwareVersions: description: Image preinstalled version version metadata. type: object @@ -266,26 +273,40 @@ components: enum: - NVIDIA - AMD - gpuDriver: + gpuModel: + description: A GPU model number. + type: string + gpuModelList: + description: A list of GPU model numbers. + type: array + items: + $ref: '#/components/schemas/gpuModel' + imageGpu: description: The GPU driver if installed. type: object required: - vendor - - version + - driver properties: vendor: $ref: '#/components/schemas/gpuVendor' - version: + driver: description: The GPU driver version, this is vendor specific. type: string + models: + $ref: '#/components/schemas/gpuModelList' imageSpec: description: An image. type: object + required: + - virtualization properties: + virtualization: + $ref: '#/components/schemas/imageVirtualization' softwareVersions: $ref: '#/components/schemas/softwareVersions' - gpuDriver: - $ref: '#/components/schemas/gpuDriver' + gpu: + $ref: '#/components/schemas/imageGpu' image: description: An image. type: object @@ -569,11 +590,16 @@ components: name: ubu2204-v1.25.6-gpu-525.85.05-7ced4154 creationTime: 2023-02-22T12:04:13Z spec: + virtualization: virtualized softwareVersions: kubernetes: v1.25.6 - gpuDriver: + gpu: vendor: NVIDIA - version: 525.85.05 + driver: 525.85.05 + models: + - A100 + - H100 + - H200 flavorsResponse: description: A list of flavors. content: diff --git a/pkg/openapi/types.go b/pkg/openapi/types.go index 4519ed8..01497a9 100644 --- a/pkg/openapi/types.go +++ b/pkg/openapi/types.go @@ -17,6 +17,13 @@ const ( NVIDIA GpuVendor = "NVIDIA" ) +// Defines values for ImageVirtualization. +const ( + Any ImageVirtualization = "any" + Baremetal ImageVirtualization = "baremetal" + Virtualized ImageVirtualization = "virtualized" +) + // Defines values for RegionType. const ( Openstack RegionType = "openstack" @@ -66,14 +73,11 @@ type FlavorSpec struct { // Flavors A list of flavors. type Flavors = []Flavor -// GpuDriver The GPU driver if installed. -type GpuDriver struct { - // Vendor The GPU vendor. - Vendor GpuVendor `json:"vendor"` +// GpuModel A GPU model number. +type GpuModel = string - // Version The GPU driver version, this is vendor specific. - Version string `json:"version"` -} +// GpuModelList A list of GPU model numbers. +type GpuModelList = []GpuModel // GpuSpec GPU specification. type GpuSpec struct { @@ -167,15 +171,33 @@ type Image struct { Spec ImageSpec `json:"spec"` } +// ImageGpu The GPU driver if installed. +type ImageGpu struct { + // Driver The GPU driver version, this is vendor specific. + Driver string `json:"driver"` + + // Models A list of GPU model numbers. + Models *GpuModelList `json:"models,omitempty"` + + // Vendor The GPU vendor. + Vendor GpuVendor `json:"vendor"` +} + // ImageSpec An image. type ImageSpec struct { - // GpuDriver The GPU driver if installed. - GpuDriver *GpuDriver `json:"gpuDriver,omitempty"` + // Gpu The GPU driver if installed. + Gpu *ImageGpu `json:"gpu,omitempty"` // SoftwareVersions Image preinstalled version version metadata. SoftwareVersions *SoftwareVersions `json:"softwareVersions,omitempty"` + + // Virtualization What type of machine the image is for. + Virtualization ImageVirtualization `json:"virtualization"` } +// ImageVirtualization What type of machine the image is for. +type ImageVirtualization string + // Images A list of images that are compatible with this platform. type Images = []Image diff --git a/pkg/providers/openstack/compute.go b/pkg/providers/openstack/compute.go index 26f13e4..0ee6771 100644 --- a/pkg/providers/openstack/compute.go +++ b/pkg/providers/openstack/compute.go @@ -107,6 +107,22 @@ func (c *ComputeClient) Flavors(ctx context.Context) ([]flavors.Flavor, error) { return nil, err } + // ************************************************************************* + // HACK HACK HACK + // ************************************************************************* + for i := range result { + f := &result[i] + + if f.ID == "c9b3b8c6-7268-4ed3-98d3-76743e3436cf" { + f.VCPUs = 128 + f.RAM = 2 * 1024 * 1024 + } + + } + // ************************************************************************* + // HACK HACK HACK + // ************************************************************************* + result = slices.DeleteFunc(result, func(flavor flavors.Flavor) bool { // We are admin, so see all the things, throw out private flavors. // TODO: we _could_ allow if our project is in the allowed IDs. diff --git a/pkg/providers/openstack/image.go b/pkg/providers/openstack/image.go index f957dfe..0402365 100644 --- a/pkg/providers/openstack/image.go +++ b/pkg/providers/openstack/image.go @@ -81,11 +81,11 @@ func NewImageClient(ctx context.Context, provider CredentialProvider, options *u } func (c *ImageClient) validateProperties(image *images.Image) bool { - if c.options == nil { + if c.options == nil || c.options.Selector == nil { return true } - for _, r := range c.options.PropertiesInclude { + for _, r := range c.options.Selector.Properties { if !slices.Contains(util.Keys(image.Properties), r) { return false } @@ -95,7 +95,7 @@ func (c *ImageClient) validateProperties(image *images.Image) bool { } func (c *ImageClient) decodeSigningKey() (*ecdsa.PublicKey, error) { - pemBlock, _ := pem.Decode(c.options.SigningKey) + pemBlock, _ := pem.Decode(c.options.Selector.SigningKey) if pemBlock == nil { return nil, ErrPEMDecode } @@ -119,7 +119,7 @@ func (c *ImageClient) decodeSigningKey() (*ecdsa.PublicKey, error) { // verifyImage asserts the image is trustworthy for use with our goodselves. func (c *ImageClient) verifyImage(image *images.Image) bool { - if c.options == nil || c.options.SigningKey == nil { + if c.options == nil || c.options.Selector == nil || c.options.Selector.SigningKey == nil { return true } @@ -129,7 +129,7 @@ func (c *ImageClient) verifyImage(image *images.Image) bool { // These will be digitally signed by Baski when created, so we only trust // those images. - signatureRaw, ok := image.Properties["digest"] + signatureRaw, ok := image.Properties["unikorn:digest"] if !ok { return false } @@ -154,24 +154,24 @@ func (c *ImageClient) verifyImage(image *images.Image) bool { return ecdsa.VerifyASN1(signingKey, hash[:], signature) } -func (c *ImageClient) imageValid(image *images.Image) bool { +func (c *ImageClient) filterImage(image *images.Image) bool { if image.Status != "active" { - return false + return true } if !c.validateProperties(image) { - return false + return true } if !c.verifyImage(image) { - return false + return true } - return true + return false } -// Images returns a list of images. -func (c *ImageClient) Images(ctx context.Context) ([]images.Image, error) { +// images does a memoized lookup of images. +func (c *ImageClient) images(ctx context.Context) ([]images.Image, error) { if result, ok := c.imageCache.Get(); ok { return result, nil } @@ -195,9 +195,21 @@ func (c *ImageClient) Images(ctx context.Context) ([]images.Image, error) { return nil, err } + c.imageCache.Set(result) + + return result, nil +} + +// Images returns a list of images. +func (c *ImageClient) Images(ctx context.Context) ([]images.Image, error) { + result, err := c.images(ctx) + if err != nil { + return nil, err + } + // Filter out images that aren't compatible. result = slices.DeleteFunc(result, func(image images.Image) bool { - return !c.imageValid(&image) + return c.filterImage(&image) }) // Sort by age, the newest should have the fewest CVEs! @@ -205,7 +217,5 @@ func (c *ImageClient) Images(ctx context.Context) ([]images.Image, error) { return a.CreatedAt.Compare(b.CreatedAt) }) - c.imageCache.Set(result) - return result, nil } diff --git a/pkg/providers/openstack/network.go b/pkg/providers/openstack/network.go index 0f1b7f2..7e0b1f6 100644 --- a/pkg/providers/openstack/network.go +++ b/pkg/providers/openstack/network.go @@ -20,6 +20,7 @@ package openstack import ( "context" "errors" + "slices" "time" "github.com/gophercloud/gophercloud/v2" @@ -48,9 +49,9 @@ var ( type NetworkClient struct { client *gophercloud.ServiceClient - externalNetworkCache *cache.TimeoutCache[[]networks.Network] - options *unikornv1.RegionOpenstackNetworkSpec + + externalNetworkCache *cache.TimeoutCache[[]networks.Network] } // NewNetworkClient provides a simple one-liner to start networking. @@ -67,6 +68,7 @@ func NewNetworkClient(ctx context.Context, provider CredentialProvider, options c := &NetworkClient{ client: client, + options: options, externalNetworkCache: cache.New[[]networks.Network](time.Hour), } @@ -79,8 +81,8 @@ func NewTestNetworkClient(options *unikornv1.RegionOpenstackNetworkSpec) *Networ } } -// ExternalNetworks returns a list of external networks. -func (c *NetworkClient) ExternalNetworks(ctx context.Context) ([]networks.Network, error) { +// externalNetworks does a memoized lookup of external networks. +func (c *NetworkClient) externalNetworks(ctx context.Context) ([]networks.Network, error) { if result, ok := c.externalNetworkCache.Get(); ok { return result, nil } @@ -97,15 +99,52 @@ func (c *NetworkClient) ExternalNetworks(ctx context.Context) ([]networks.Networ return nil, err } - var results []networks.Network + var result []networks.Network + + if err := networks.ExtractNetworksInto(page, &result); err != nil { + return nil, err + } + + c.externalNetworkCache.Set(result) + + return result, nil +} + +// filterExternalNetwork returns true if the image should be filtered. +func (c *NetworkClient) filterExternalNetwork(network *networks.Network) bool { + if c.options == nil || c.options.ExternalNetworks == nil || c.options.ExternalNetworks.Selector == nil { + return false + } + + if c.options.ExternalNetworks.Selector.IDs != nil { + if !slices.Contains(c.options.ExternalNetworks.Selector.IDs, network.ID) { + return true + } + } - if err := networks.ExtractNetworksInto(page, &results); err != nil { + if c.options.ExternalNetworks.Selector.Tags != nil { + for _, tag := range c.options.ExternalNetworks.Selector.Tags { + if !slices.Contains(network.Tags, tag) { + return true + } + } + } + + return false +} + +// ExternalNetworks returns a list of external networks. +func (c *NetworkClient) ExternalNetworks(ctx context.Context) ([]networks.Network, error) { + result, err := c.externalNetworks(ctx) + if err != nil { return nil, err } - c.externalNetworkCache.Set(results) + result = slices.DeleteFunc(result, func(network networks.Network) bool { + return c.filterExternalNetwork(&network) + }) - return results, nil + return result, nil } // AllocateVLAN does exactly that using configured ID ranges and existing networks. @@ -114,12 +153,12 @@ func (c *NetworkClient) AllocateVLAN(ctx context.Context) (int, error) { // 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.VLAN == nil || c.options.VLAN.Segments == nil { + 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.VLAN.Segments { + for _, segment := range c.options.ProviderNetworks.VLAN.Segments { for i := segment.StartID; i < segment.EndID+1; i++ { allocatable[i] = true } @@ -138,7 +177,7 @@ 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) (int, *networks.Network, error) { - if c.options == nil || c.options.PhysicalNetwork == nil { + if c.options == nil || c.options.ProviderNetworks == nil || c.options.ProviderNetworks.PhysicalNetwork == nil { return -1, nil, ErrConfiguration } @@ -161,7 +200,7 @@ func (c *NetworkClient) CreateVLANProviderNetwork(ctx context.Context, name stri Segments: []provider.Segment{ { NetworkType: "vlan", - PhysicalNetwork: *c.options.PhysicalNetwork, + PhysicalNetwork: *c.options.ProviderNetworks.PhysicalNetwork, SegmentationID: vlanID, }, }, diff --git a/pkg/providers/openstack/network_test.go b/pkg/providers/openstack/network_test.go index 46deee6..fa871a4 100644 --- a/pkg/providers/openstack/network_test.go +++ b/pkg/providers/openstack/network_test.go @@ -40,11 +40,13 @@ func TestVLANAllocateRanges(t *testing.T) { { name: "SingleSegment", options: &unikornv1.RegionOpenstackNetworkSpec{ - VLAN: &unikornv1.VLANSpec{ - Segments: []unikornv1.VLANSegment{ - { - StartID: 100, - EndID: 200, + ProviderNetworks: &unikornv1.ProviderNetworks{ + VLAN: &unikornv1.VLANSpec{ + Segments: []unikornv1.VLANSegment{ + { + StartID: 100, + EndID: 200, + }, }, }, }, diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index 0d9711c..88ec509 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -288,14 +288,6 @@ func (p *Provider) Flavors(ctx context.Context) (providers.FlavorList, error) { return result, nil } -func semver(in string) string { - if !strings.HasPrefix(in, "v") { - return "v" + in - } - - return in -} - // Images lists all available images. func (p *Provider) Images(ctx context.Context) (providers.ImageList, error) { imageService, err := p.image(ctx) @@ -308,20 +300,44 @@ func (p *Provider) Images(ctx context.Context) (providers.ImageList, error) { return nil, err } - result := make(providers.ImageList, 0, len(resources)) + result := make(providers.ImageList, len(resources)) for i := range resources { image := &resources[i] - kubernetesVersion, _ := image.Properties["k8s"].(string) + virtualization, _ := image.Properties["unikorn:virtualization"].(string) + kubernetesVersion, _ := image.Properties["unikorn:kubernetes_version"].(string) - result = append(result, providers.Image{ + providerImage := providers.Image{ ID: image.ID, Name: image.Name, Created: image.CreatedAt, Modified: image.UpdatedAt, - KubernetesVersion: semver(kubernetesVersion), - }) + Virtualization: providers.ImageVirtualization(virtualization), + KubernetesVersion: kubernetesVersion, + } + + if gpuVendor, ok := image.Properties["unikorn:gpu_vendor"].(string); ok { + gpuDriver, ok := image.Properties["unikorn:gpu_driver_version"].(string) + if !ok { + // TODO: it's perhaps better to just skip this one, rather than + // kill the entire service?? + return nil, fmt.Errorf("%w: GPU driver is not defined for image %s", ErrKeyUndefined, image.ID) + } + + gpu := &providers.ImageGPU{ + Vendor: providers.GPUVendor(gpuVendor), + Driver: gpuDriver, + } + + if models, ok := image.Properties["unikorn:gpu_models"].(string); ok { + gpu.Models = strings.Split(models, ",") + } + + providerImage.GPU = gpu + } + + result[i] = providerImage } return result, nil diff --git a/pkg/providers/types.go b/pkg/providers/types.go index e344a34..a46663b 100644 --- a/pkg/providers/types.go +++ b/pkg/providers/types.go @@ -66,6 +66,14 @@ type GPU struct { // FlavorList allows us to attach sort functions and the like. type FlavorList []Flavor +type ImageVirtualization string + +const ( + Virtualized ImageVirtualization = "virtualized" + Baremetal ImageVirtualization = "baremetal" + Any ImageVirtualization = "any" +) + // Image represents an operating system image. type Image struct { // ID must be an immutable ID, preferably a UUID. @@ -78,11 +86,25 @@ type Image struct { Created time.Time // Modified is when the image was modified. Modified time.Time + // ImageVirtualization defines how the image can be used. + Virtualization ImageVirtualization // KubernetesVersion is only populated if the image contains a pre-installed // version of Kubernetes, this acts as a cache and improves provisioning performance. // This is pretty much the only source of truth about Kubernetes versions at // present, so should be populated. It must be a semver (starts with a vN.N.N). KubernetesVersion string + // GPU is any GPU specific configuration for scheduling on a specific flavor type. + GPU *ImageGPU +} + +// ImageGPU defines image specific GPU compatibility information. +type ImageGPU struct { + // Vendor is the vendor a GPU is compatible with. + Vendor GPUVendor + // Driver is the driver version string. + Driver string + // Models is a list of GPU models a driver is certified with. + Models []string } // ImageList allows us to attach sort functions and the like.