From 43c5d8630eae19892611f363f02846076f6f8802 Mon Sep 17 00:00:00 2001 From: Simon Murray Date: Fri, 9 Aug 2024 14:00:55 +0100 Subject: [PATCH] Separate Openstack from Identity As previously commented, having openstack state in a controlled resource is a bad idea. This separates stuff out so that a shadow record is created to hold state, so the managed resource is not required to be updated. --- .../region.unikorn-cloud.org_identities.yaml | 34 --- ...unikorn-cloud.org_openstackidentities.yaml | 87 ++++++++ .../identity-controller/clusterrole.yaml | 11 +- .../region-controller/clusterrole.yaml | 7 + .../templates/region-controller/ingress.yaml | 5 + go.mod | 6 + go.sum | 10 +- pkg/apis/unikorn/v1alpha1/register.go | 1 + pkg/apis/unikorn/v1alpha1/types.go | 35 +++- .../unikorn/v1alpha1/zz_generated.deepcopy.go | 194 ++++++++++++------ pkg/client/client.go | 97 ++------- pkg/handler/handler.go | 38 ++-- pkg/managers/identity/manager.go | 9 +- pkg/providers/openstack/provider.go | 170 +++++++++++---- .../managers/identity/provisioner.go | 33 +-- pkg/server/server.go | 15 +- 16 files changed, 468 insertions(+), 284 deletions(-) create mode 100644 charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml diff --git a/charts/region/crds/region.unikorn-cloud.org_identities.yaml b/charts/region/crds/region.unikorn-cloud.org_identities.yaml index 8dc394b..1fcae08 100644 --- a/charts/region/crds/region.unikorn-cloud.org_identities.yaml +++ b/charts/region/crds/region.unikorn-cloud.org_identities.yaml @@ -57,40 +57,6 @@ spec: spec: description: IdentitySpec stores any state necessary to manage identity. properties: - openstack: - description: OpenStack is populated when the provider type is set - to "openstack". - properties: - applicationCredentialID: - description: ApplicationCredentialID is the ID of the user's application - credential. - type: string - applicationCredentialSecret: - description: ApplicationCredentialSecret is the one-time secret - for the application credential. - type: string - cloud: - description: Cloud is the cloud name in the cloud config to use. - type: string - cloudConfig: - description: CloudConfig is a client compatible cloud configuration. - format: byte - type: string - password: - description: Password is the login for the user. - type: string - projectID: - description: ProjectID is the ID of the project created for the - identity. - type: string - serverGroupID: - description: ServerGroupID is the ID of the server group created - for the identity. - type: string - userID: - description: UserID is the ID of the user created for the identity. - type: string - type: object pause: description: Pause, if true, will inhibit reconciliation. type: boolean diff --git a/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml b/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml new file mode 100644 index 0000000..8f232cd --- /dev/null +++ b/charts/region/crds/region.unikorn-cloud.org_openstackidentities.yaml @@ -0,0 +1,87 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: openstackidentities.region.unikorn-cloud.org +spec: + group: region.unikorn-cloud.org + names: + categories: + - unikorn + kind: OpenstackIdentity + listKind: OpenstackIdentityList + plural: openstackidentities + singular: openstackidentity + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .spec.provider + name: provider + type: string + - jsonPath: .metadata.creationTimestamp + name: age + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: OpenstackIdentity has no controller, its a database record of + state. + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + properties: + applicationCredentialID: + description: ApplicationCredentialID is the ID of the user's application + credential. + type: string + applicationCredentialSecret: + description: ApplicationCredentialSecret is the one-time secret for + the application credential. + type: string + cloud: + description: Cloud is the cloud name in the cloud config to use. + type: string + cloudConfig: + description: CloudConfig is a client compatible cloud configuration. + format: byte + type: string + password: + description: Password is the login for the user. + type: string + projectID: + description: ProjectID is the ID of the project created for the identity. + type: string + serverGroupID: + description: ServerGroupID is the ID of the server group created for + the identity. + type: string + userID: + description: UserID is the ID of the user created for the identity. + type: string + type: object + status: + type: object + required: + - spec + type: object + served: true + storage: true + subresources: {} diff --git a/charts/region/templates/identity-controller/clusterrole.yaml b/charts/region/templates/identity-controller/clusterrole.yaml index 916bee5..ab41885 100644 --- a/charts/region/templates/identity-controller/clusterrole.yaml +++ b/charts/region/templates/identity-controller/clusterrole.yaml @@ -21,6 +21,16 @@ rules: - identities/status verbs: - update +- apiGroups: + - region.unikorn-cloud.org + resources: + - openstackidentities + verbs: + - list + - watch + - create + - update + - delete - apiGroups: - "" resources: @@ -35,4 +45,3 @@ rules: verbs: - list - watch - diff --git a/charts/region/templates/region-controller/clusterrole.yaml b/charts/region/templates/region-controller/clusterrole.yaml index 729b7f3..dbf86bc 100644 --- a/charts/region/templates/region-controller/clusterrole.yaml +++ b/charts/region/templates/region-controller/clusterrole.yaml @@ -22,6 +22,13 @@ rules: - watch - create - delete +- apiGroups: + - region.unikorn-cloud.org + resources: + - openstackidentities + verbs: + - list + - watch - apiGroups: - "" resources: diff --git a/charts/region/templates/region-controller/ingress.yaml b/charts/region/templates/region-controller/ingress.yaml index fb4f0de..16418c6 100644 --- a/charts/region/templates/region-controller/ingress.yaml +++ b/charts/region/templates/region-controller/ingress.yaml @@ -6,6 +6,11 @@ metadata: {{- include "unikorn.labels" . | nindent 4 }} annotations: {{- include "unikorn.ingress.clusterIssuer.annotations" . | nindent 4 }} + # mTLS is used to authenticate other Unikorn services + nginx.ingress.kubernetes.io/auth-tls-verify-client: optional + nginx.ingress.kubernetes.io/auth-tls-secret: cert-manager/unikorn-client-ca + nginx.ingress.kubernetes.io/auth-tls-verify-depth: "1" + nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" {{- if (include "unikorn.ingress.externalDNS" .) }} external-dns.alpha.kubernetes.io/hostname: {{ include "unikorn.region.host" . }} {{- end }} diff --git a/go.mod b/go.mod index 41eed32..4031704 100644 --- a/go.mod +++ b/go.mod @@ -96,3 +96,9 @@ require ( sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) + +replace github.com/unikorn-cloud/core => github.com/unikorn-cloud/core v0.1.65-0.20240813085140-a05a6cc08ec0 + +replace github.com/unikorn-cloud/identity => github.com/unikorn-cloud/identity v0.2.30-0.20240814141651-b9fd2252971e + +replace github.com/unikorn-cloud/region => github.com/unikorn-cloud/region v0.1.33-0.20240813105551-7bcbcff4171e diff --git a/go.sum b/go.sum index eb4f2c2..440ee69 100644 --- a/go.sum +++ b/go.sum @@ -138,12 +138,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/unikorn-cloud/core v0.1.63 h1:Jl/xuoGRKESMXhS1+apcaS/1I776agTyT75BGz9AKBA= -github.com/unikorn-cloud/core v0.1.63/go.mod h1:JcUIQW3+oiZPUQmOlENw3OCi35IBxPKa+J4MbP3TO7k= -github.com/unikorn-cloud/core v0.1.64 h1:4GqACg1YOmUrDo5euXz0yi753Mzx1TBg7fioRKysb1g= -github.com/unikorn-cloud/core v0.1.64/go.mod h1:JcUIQW3+oiZPUQmOlENw3OCi35IBxPKa+J4MbP3TO7k= -github.com/unikorn-cloud/identity v0.2.29 h1:kKEJmh6tjjdvZWYdZhyRewG3aHf9wmWwG5C/kb+Rm9A= -github.com/unikorn-cloud/identity v0.2.29/go.mod h1:ujrL+6kRUrPIk4Z0Yc12A+FDy6L4b2Hgzz6oGZlKfGI= +github.com/unikorn-cloud/core v0.1.65-0.20240813085140-a05a6cc08ec0 h1:bSt/UfAZCtEruyPqPmK485ybuRBjbhzl/JDnGvZUvcY= +github.com/unikorn-cloud/core v0.1.65-0.20240813085140-a05a6cc08ec0/go.mod h1:JcUIQW3+oiZPUQmOlENw3OCi35IBxPKa+J4MbP3TO7k= +github.com/unikorn-cloud/identity v0.2.30-0.20240814141651-b9fd2252971e h1:VS735VW16SbuLmUiHBrhcRWIyuqr7Bsfdtup3nmLoGk= +github.com/unikorn-cloud/identity v0.2.30-0.20240814141651-b9fd2252971e/go.mod h1:V8tke7F57cB5OVGfRBy76Sv8qZ8g1ShqJ+ZhhbzBBmU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= diff --git a/pkg/apis/unikorn/v1alpha1/register.go b/pkg/apis/unikorn/v1alpha1/register.go index 77de2bb..fde2a74 100644 --- a/pkg/apis/unikorn/v1alpha1/register.go +++ b/pkg/apis/unikorn/v1alpha1/register.go @@ -50,6 +50,7 @@ var ( func init() { SchemeBuilder.Register(&Region{}, &RegionList{}) SchemeBuilder.Register(&Identity{}, &IdentityList{}) + SchemeBuilder.Register(&OpenstackIdentity{}, &OpenstackIdentityList{}) SchemeBuilder.Register(&PhysicalNetwork{}, &PhysicalNetworkList{}) } diff --git a/pkg/apis/unikorn/v1alpha1/types.go b/pkg/apis/unikorn/v1alpha1/types.go index 6c97854..29d82d9 100644 --- a/pkg/apis/unikorn/v1alpha1/types.go +++ b/pkg/apis/unikorn/v1alpha1/types.go @@ -299,11 +299,35 @@ type IdentitySpec struct { Tags TagList `json:"tags,omitempty"` // Provider defines the provider type. Provider Provider `json:"provider"` - // OpenStack is populated when the provider type is set to "openstack". - OpenStack *IdentitySpecOpenStack `json:"openstack,omitempty"` } -type IdentitySpecOpenStack struct { +type IdentityStatus struct { + // Current service state of a cluster manager. + Conditions []unikornv1core.Condition `json:"conditions,omitempty"` +} + +// OpenstackIdentityList is a typed list of identities. +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +type OpenstackIdentityList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []OpenstackIdentity `json:"items"` +} + +// OpenstackIdentity has no controller, its a database record of state. +// +genclient +// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object +// +kubebuilder:resource:scope=Namespaced,categories=unikorn +// +kubebuilder:printcolumn:name="provider",type="string",JSONPath=".spec.provider" +// +kubebuilder:printcolumn:name="age",type="date",JSONPath=".metadata.creationTimestamp" +type OpenstackIdentity struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + Spec OpenstackIdentitySpec `json:"spec"` + Status OpenstackIdentityStatus `json:"status,omitempty"` +} + +type OpenstackIdentitySpec struct { // CloudConfig is a client compatible cloud configuration. CloudConfig []byte `json:"cloudConfig,omitempty"` // Cloud is the cloud name in the cloud config to use. @@ -322,10 +346,7 @@ type IdentitySpecOpenStack struct { ServerGroupID *string `json:"serverGroupID,omitempty"` } -type IdentityStatus struct { - // Current service state of a cluster manager. - Conditions []unikornv1core.Condition `json:"conditions,omitempty"` -} +type OpenstackIdentityStatus struct{} // PhysicalNetworkList s a typed list of physical networks. // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go index fdc3e78..4e23872 100644 --- a/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/unikorn/v1alpha1/zz_generated.deepcopy.go @@ -215,11 +215,6 @@ func (in *IdentitySpec) DeepCopyInto(out *IdentitySpec) { *out = make(TagList, len(*in)) copy(*out, *in) } - if in.OpenStack != nil { - in, out := &in.OpenStack, &out.OpenStack - *out = new(IdentitySpecOpenStack) - (*in).DeepCopyInto(*out) - } return } @@ -233,62 +228,6 @@ func (in *IdentitySpec) DeepCopy() *IdentitySpec { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *IdentitySpecOpenStack) DeepCopyInto(out *IdentitySpecOpenStack) { - *out = *in - if in.CloudConfig != nil { - in, out := &in.CloudConfig, &out.CloudConfig - *out = make([]byte, len(*in)) - copy(*out, *in) - } - if in.Cloud != nil { - in, out := &in.Cloud, &out.Cloud - *out = new(string) - **out = **in - } - if in.UserID != nil { - in, out := &in.UserID, &out.UserID - *out = new(string) - **out = **in - } - if in.Password != nil { - in, out := &in.Password, &out.Password - *out = new(string) - **out = **in - } - if in.ProjectID != nil { - in, out := &in.ProjectID, &out.ProjectID - *out = new(string) - **out = **in - } - if in.ApplicationCredentialID != nil { - in, out := &in.ApplicationCredentialID, &out.ApplicationCredentialID - *out = new(string) - **out = **in - } - if in.ApplicationCredentialSecret != nil { - in, out := &in.ApplicationCredentialSecret, &out.ApplicationCredentialSecret - *out = new(string) - **out = **in - } - if in.ServerGroupID != nil { - in, out := &in.ServerGroupID, &out.ServerGroupID - *out = new(string) - **out = **in - } - return -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new IdentitySpecOpenStack. -func (in *IdentitySpecOpenStack) DeepCopy() *IdentitySpecOpenStack { - if in == nil { - return nil - } - out := new(IdentitySpecOpenStack) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *IdentityStatus) DeepCopyInto(out *IdentityStatus) { *out = *in @@ -408,6 +347,139 @@ func (in *OpenstackFlavorsSpec) DeepCopy() *OpenstackFlavorsSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentity) DeepCopyInto(out *OpenstackIdentity) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + out.Status = in.Status + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentity. +func (in *OpenstackIdentity) DeepCopy() *OpenstackIdentity { + if in == nil { + return nil + } + out := new(OpenstackIdentity) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackIdentity) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentityList) DeepCopyInto(out *OpenstackIdentityList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]OpenstackIdentity, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentityList. +func (in *OpenstackIdentityList) DeepCopy() *OpenstackIdentityList { + if in == nil { + return nil + } + out := new(OpenstackIdentityList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *OpenstackIdentityList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentitySpec) DeepCopyInto(out *OpenstackIdentitySpec) { + *out = *in + if in.CloudConfig != nil { + in, out := &in.CloudConfig, &out.CloudConfig + *out = make([]byte, len(*in)) + copy(*out, *in) + } + if in.Cloud != nil { + in, out := &in.Cloud, &out.Cloud + *out = new(string) + **out = **in + } + if in.UserID != nil { + in, out := &in.UserID, &out.UserID + *out = new(string) + **out = **in + } + if in.Password != nil { + in, out := &in.Password, &out.Password + *out = new(string) + **out = **in + } + if in.ProjectID != nil { + in, out := &in.ProjectID, &out.ProjectID + *out = new(string) + **out = **in + } + if in.ApplicationCredentialID != nil { + in, out := &in.ApplicationCredentialID, &out.ApplicationCredentialID + *out = new(string) + **out = **in + } + if in.ApplicationCredentialSecret != nil { + in, out := &in.ApplicationCredentialSecret, &out.ApplicationCredentialSecret + *out = new(string) + **out = **in + } + if in.ServerGroupID != nil { + in, out := &in.ServerGroupID, &out.ServerGroupID + *out = new(string) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentitySpec. +func (in *OpenstackIdentitySpec) DeepCopy() *OpenstackIdentitySpec { + if in == nil { + return nil + } + out := new(OpenstackIdentitySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OpenstackIdentityStatus) DeepCopyInto(out *OpenstackIdentityStatus) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OpenstackIdentityStatus. +func (in *OpenstackIdentityStatus) DeepCopy() *OpenstackIdentityStatus { + if in == nil { + return nil + } + out := new(OpenstackIdentityStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *OpenstackProviderNetworkSpec) DeepCopyInto(out *OpenstackProviderNetworkSpec) { *out = *in diff --git a/pkg/client/client.go b/pkg/client/client.go index 70cd864..7f53ac8 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,36 +18,24 @@ package client import ( "context" - "crypto/tls" - "crypto/x509" - "errors" - "fmt" "net/http" - "github.com/spf13/pflag" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/propagation" "github.com/unikorn-cloud/core/pkg/authorization/accesstoken" + coreclient "github.com/unikorn-cloud/core/pkg/client" + "github.com/unikorn-cloud/identity/pkg/middleware/authorization" "github.com/unikorn-cloud/region/pkg/openapi" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" ) -var ( - // ErrFormatError is returned when a secret doesn't meet the specification. - ErrFormatError = errors.New("secret incorrectly formatted") -) +type Options = coreclient.HTTPOptions -type Options struct { - // host is the region host name. - host string - // caSecretNamespace tells us where to source the CA secret. - caSecretNamespace string - // caSecretName is the root CA secret of the region endpoint. - caSecretName string +// NewOptions must be used to create options for consistency. +func NewOptions() *Options { + return coreclient.NewHTTPOptions("region") } // Client wraps up the raw OpenAPI client with things to make it useable e.g. @@ -55,77 +43,28 @@ type Options struct { type Client struct { // client is a Kubenetes client. client client.Client - // namespace is the namespace the client is running in. - namespace string // options allows setting of option from the CLI options *Options -} - -// AddFlags adds the options to the CLI flags. -func (o *Options) AddFlags(f *pflag.FlagSet) { - f.StringVar(&o.host, "region-host", "", "Region endpoint URL.") - f.StringVar(&o.caSecretNamespace, "region-ca-secret-namespace", "", "Region endpoint CA certificate secret namespace.") - f.StringVar(&o.caSecretName, "region-ca-secret-name", "", "Region endpoint CA certificate secret.") + // clientOptions may be specified to inject client certificates etc. + clientOptions *coreclient.HTTPClientOptions } // New creates a new client. -func New(client client.Client, namespace string, options *Options) *Client { +func New(client client.Client, options *Options, clientOptions *coreclient.HTTPClientOptions) *Client { return &Client{ - client: client, - namespace: namespace, - options: options, + client: client, + options: options, + clientOptions: clientOptions, } } -// tlsClientConfig abstracts away private TLS CAs or self signed certificates. -func (c *Client) tlsClientConfig(ctx context.Context) (*tls.Config, error) { - if c.options.caSecretName == "" { - //nolint:nilnil - return nil, nil - } - - namespace := c.namespace - - if c.options.caSecretNamespace != "" { - namespace = c.options.caSecretNamespace - } - - secret := &corev1.Secret{} - - if err := c.client.Get(ctx, client.ObjectKey{Namespace: namespace, Name: c.options.caSecretName}, secret); err != nil { - return nil, err - } - - if secret.Type != corev1.SecretTypeTLS { - return nil, fmt.Errorf("%w: issuer CA not of type kubernetes.io/tls", ErrFormatError) - } - - cert, ok := secret.Data[corev1.TLSCertKey] - if !ok { - return nil, fmt.Errorf("%w: issuer CA missing tls.crt", ErrFormatError) - } - - certPool := x509.NewCertPool() - - if ok := certPool.AppendCertsFromPEM(cert); !ok { - return nil, fmt.Errorf("%w: failed to load region CA certificate", ErrFormatError) - } - - config := &tls.Config{ - RootCAs: certPool, - MinVersion: tls.VersionTLS13, - } - - return config, nil -} - -// httpClient returns a new http client that will transparently do oauth2 header +// HTTPClient returns a new http client that will transparently do oauth2 header // injection and refresh token updates. -func (c *Client) httpClient(ctx context.Context) (*http.Client, error) { +func (c *Client) HTTPClient(ctx context.Context) (*http.Client, error) { // Handle non-system CA certificates for the OIDC discovery protocol // and oauth2 token refresh. This will return nil if none is specified // and default to the system roots. - tlsClientConfig, err := c.tlsClientConfig(ctx) + tlsClientConfig, err := coreclient.TLSClientConfig(ctx, c.client, c.options, c.clientOptions) if err != nil { return nil, err } @@ -142,20 +81,20 @@ func (c *Client) httpClient(ctx context.Context) (*http.Client, error) { // accessTokenInjector implements OAuth2 bearer token authorization. func accessTokenInjector(ctx context.Context, req *http.Request) error { req.Header.Set("Authorization", "bearer "+accesstoken.FromContext(ctx)) - otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header)) + authorization.InjectClientCert(ctx, req.Header) return nil } // Client returns a new OpenAPI client that can be used to access the API. func (c *Client) Client(ctx context.Context) (*openapi.ClientWithResponses, error) { - httpClient, err := c.httpClient(ctx) + httpClient, err := c.HTTPClient(ctx) if err != nil { return nil, err } - client, err := openapi.NewClientWithResponses(c.options.host, openapi.WithHTTPClient(httpClient), openapi.WithRequestEditorFn(accessTokenInjector)) + client, err := openapi.NewClientWithResponses(c.options.Host(), openapi.WithHTTPClient(httpClient), openapi.WithRequestEditorFn(accessTokenInjector)) if err != nil { return nil, err } diff --git a/pkg/handler/handler.go b/pkg/handler/handler.go index 237776d..4798499 100644 --- a/pkg/handler/handler.go +++ b/pkg/handler/handler.go @@ -27,6 +27,7 @@ import ( "slices" "time" + unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" coreconstants "github.com/unikorn-cloud/core/pkg/constants" coreapi "github.com/unikorn-cloud/core/pkg/openapi" "github.com/unikorn-cloud/core/pkg/server/conversion" @@ -297,9 +298,15 @@ func convertTags(in unikornv1.TagList) openapi.TagList { return out } -func convertIdentity(in *unikornv1.Identity) *openapi.IdentityRead { +func (h *Handler) convertIdentity(ctx context.Context, in *unikornv1.Identity) *openapi.IdentityRead { + provisioningStatus := coreapi.ResourceProvisioningStatusUnknown + + if condition, err := in.StatusConditionRead(unikornv1core.ConditionAvailable); err == nil { + provisioningStatus = conversion.ConvertStatusCondition(condition) + } + out := &openapi.IdentityRead{ - Metadata: conversion.ProjectScopedResourceReadMetadata(in, coreapi.ResourceProvisioningStatusProvisioned), + Metadata: conversion.ProjectScopedResourceReadMetadata(in, provisioningStatus), Spec: openapi.IdentitySpec{ RegionId: in.Labels[constants.RegionLabel], }, @@ -313,32 +320,31 @@ func convertIdentity(in *unikornv1.Identity) *openapi.IdentityRead { case unikornv1.ProviderOpenstack: out.Spec.Type = openapi.Openstack - if in.Spec.OpenStack != nil { + var openstackIdentity unikornv1.OpenstackIdentity + + if err := h.client.Get(ctx, client.ObjectKey{Namespace: in.Namespace, Name: in.Name}, &openstackIdentity); err == nil { out.Spec.Openstack = &openapi.IdentitySpecOpenStack{ - Cloud: in.Spec.OpenStack.Cloud, - UserId: in.Spec.OpenStack.UserID, - ProjectId: in.Spec.OpenStack.ProjectID, + Cloud: openstackIdentity.Spec.Cloud, + UserId: openstackIdentity.Spec.UserID, + ProjectId: openstackIdentity.Spec.ProjectID, + ServerGroupId: openstackIdentity.Spec.ServerGroupID, } - if in.Spec.OpenStack.CloudConfig != nil { - cloudConfig := base64.URLEncoding.EncodeToString(in.Spec.OpenStack.CloudConfig) + if openstackIdentity.Spec.CloudConfig != nil { + cloudConfig := base64.URLEncoding.EncodeToString(openstackIdentity.Spec.CloudConfig) out.Spec.Openstack.CloudConfig = &cloudConfig } - - if in.Spec.OpenStack.ServerGroupID != nil { - out.Spec.Openstack.ServerGroupId = in.Spec.OpenStack.ServerGroupID - } } } return out } -func convertIdentityList(in unikornv1.IdentityList) openapi.IdentitiesRead { +func (h *Handler) convertIdentityList(ctx context.Context, in unikornv1.IdentityList) openapi.IdentitiesRead { out := make(openapi.IdentitiesRead, len(in.Items)) for i := range in.Items { - out[i] = *convertIdentity(&in.Items[i]) + out[i] = *h.convertIdentity(ctx, &in.Items[i]) } return out @@ -367,7 +373,7 @@ func (h *Handler) GetApiV1OrganizationsOrganizationIDIdentities(w http.ResponseW return cmp.Compare(a.Name, b.Name) }) - util.WriteJSONResponse(w, r, http.StatusOK, convertIdentityList(result)) + util.WriteJSONResponse(w, r, http.StatusOK, h.convertIdentityList(r.Context(), result)) } func generateTag(in openapi.Tag) unikornv1.Tag { @@ -432,7 +438,7 @@ func (h *Handler) PostApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitie } h.setCacheable(w) - util.WriteJSONResponse(w, r, http.StatusCreated, convertIdentity(identity)) + util.WriteJSONResponse(w, r, http.StatusCreated, h.convertIdentity(r.Context(), identity)) } func (h *Handler) DeleteApiV1OrganizationsOrganizationIDProjectsProjectIDIdentitiesIdentityID(w http.ResponseWriter, r *http.Request, organizationID openapi.OrganizationIDParameter, projectID openapi.ProjectIDParameter, identityID openapi.IdentityIDParameter) { diff --git a/pkg/managers/identity/manager.go b/pkg/managers/identity/manager.go index 04db442..b3e5bf2 100644 --- a/pkg/managers/identity/manager.go +++ b/pkg/managers/identity/manager.go @@ -43,9 +43,14 @@ func (*Factory) Metadata() (string, string, string) { return constants.Application, constants.Version, constants.Revision } +// Options returns any options to be added to the CLI flags and passed to the reconciler. +func (*Factory) Options() coremanager.ControllerOptions { + return nil +} + // Reconciler returns a new reconciler instance. -func (*Factory) Reconciler(options *options.Options, manager manager.Manager) reconcile.Reconciler { - return coremanager.NewReconciler(options, manager, identity.New) +func (*Factory) Reconciler(options *options.Options, controllerOptions coremanager.ControllerOptions, manager manager.Manager) reconcile.Reconciler { + return coremanager.NewReconciler(options, controllerOptions, manager, identity.New) } // RegisterWatches adds any watches that would trigger a reconcile. diff --git a/pkg/providers/openstack/provider.go b/pkg/providers/openstack/provider.go index f75e7b9..ae6d717 100644 --- a/pkg/providers/openstack/provider.go +++ b/pkg/providers/openstack/provider.go @@ -36,10 +36,13 @@ import ( "github.com/unikorn-cloud/region/pkg/providers" corev1 "k8s.io/api/core/v1" + kerrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/uuid" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/yaml" ) @@ -368,7 +371,7 @@ const ( ) // projectTags defines how to tag projects. -func projectTags(identity *unikornv1.Identity) []string { +func projectTags(identity *unikornv1.OpenstackIdentity) []string { tags := []string{ OrganizationTag + "=" + identity.Labels[coreconstants.OrganizationLabel], ProjectTag + "=" + identity.Labels[coreconstants.ProjectLabel], @@ -377,15 +380,15 @@ func projectTags(identity *unikornv1.Identity) []string { return tags } -func identityResourceName(identity *unikornv1.Identity) string { +func identityResourceName(identity *unikornv1.OpenstackIdentity) string { return "unikorn-identity-" + identity.Name } // provisionUser creates a new user in the managed domain with a random password. // There is a 1:1 mapping of user to project, and the project name is unique in the // domain, so just reuse this, we can clean them up at the same time. -func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityClient, identity *unikornv1.Identity) error { - if identity.Spec.OpenStack.UserID != nil { +func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.UserID != nil { return nil } @@ -397,8 +400,8 @@ func (p *Provider) provisionUser(ctx context.Context, identityService *IdentityC return err } - identity.Spec.OpenStack.UserID = &user.ID - identity.Spec.OpenStack.Password = &password + identity.Spec.UserID = &user.ID + identity.Spec.Password = &password return nil } @@ -406,8 +409,8 @@ 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, identity *unikornv1.Identity) error { - if identity.Spec.OpenStack.ProjectID != nil { +func (p *Provider) provisionProject(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ProjectID != nil { return nil } @@ -418,7 +421,7 @@ func (p *Provider) provisionProject(ctx context.Context, identityService *Identi return err } - identity.Spec.OpenStack.ProjectID = &project.ID + identity.Spec.ProjectID = &project.ID return nil } @@ -453,7 +456,7 @@ func (p *Provider) getRequiredRoles() []string { // provisionProjectRoles creates a binding between our service account and the project // with the required roles to provision an application credential that will allow cluster // creation, deletion and life-cycle management. -func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, identity *unikornv1.Identity) error { +func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *IdentityClient, identity *unikornv1.OpenstackIdentity) error { allRoles, err := identityService.ListRoles(ctx) if err != nil { return err @@ -465,7 +468,7 @@ func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *I return err } - if err := identityService.CreateRoleAssignment(ctx, *identity.Spec.OpenStack.UserID, *identity.Spec.OpenStack.ProjectID, roleID); err != nil { + if err := identityService.CreateRoleAssignment(ctx, *identity.Spec.UserID, *identity.Spec.ProjectID, roleID); err != nil { return err } } @@ -473,13 +476,13 @@ func (p *Provider) provisionProjectRoles(ctx context.Context, identityService *I return nil } -func (p *Provider) provisionApplicationCredential(ctx context.Context, identity *unikornv1.Identity) error { - if identity.Spec.OpenStack.ApplicationCredentialID != nil { +func (p *Provider) provisionApplicationCredential(ctx context.Context, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ApplicationCredentialID != nil { return nil } // Rescope to the user/project... - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.OpenStack.UserID, *identity.Spec.OpenStack.Password, *identity.Spec.OpenStack.ProjectID) + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.UserID, *identity.Spec.Password, *identity.Spec.ProjectID) identityService, err := NewIdentityClient(ctx, providerClient) if err != nil { @@ -488,19 +491,19 @@ func (p *Provider) provisionApplicationCredential(ctx context.Context, identity name := identityResourceName(identity) - appcred, err := identityService.CreateApplicationCredential(ctx, *identity.Spec.OpenStack.UserID, name, "IaaS lifecycle management", p.getRequiredRoles()) + appcred, err := identityService.CreateApplicationCredential(ctx, *identity.Spec.UserID, name, "IaaS lifecycle management", p.getRequiredRoles()) if err != nil { return err } - identity.Spec.OpenStack.ApplicationCredentialID = &appcred.ID - identity.Spec.OpenStack.ApplicationCredentialSecret = &appcred.Secret + identity.Spec.ApplicationCredentialID = &appcred.ID + identity.Spec.ApplicationCredentialSecret = &appcred.Secret return nil } -func (p *Provider) createClientConfig(identity *unikornv1.Identity) error { - if identity.Spec.OpenStack.Cloud != nil { +func (p *Provider) createClientConfig(identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.Cloud != nil { return nil } @@ -512,8 +515,8 @@ func (p *Provider) createClientConfig(identity *unikornv1.Identity) error { AuthType: clientconfig.AuthV3ApplicationCredential, AuthInfo: &clientconfig.AuthInfo{ AuthURL: p.region.Spec.Openstack.Endpoint, - ApplicationCredentialID: *identity.Spec.OpenStack.ApplicationCredentialID, - ApplicationCredentialSecret: *identity.Spec.OpenStack.ApplicationCredentialSecret, + ApplicationCredentialID: *identity.Spec.ApplicationCredentialID, + ApplicationCredentialSecret: *identity.Spec.ApplicationCredentialSecret, }, }, }, @@ -524,8 +527,8 @@ func (p *Provider) createClientConfig(identity *unikornv1.Identity) error { return err } - identity.Spec.OpenStack.Cloud = &cloud - identity.Spec.OpenStack.CloudConfig = clientConfigYAML + identity.Spec.Cloud = &cloud + identity.Spec.CloudConfig = clientConfigYAML return nil } @@ -553,13 +556,13 @@ func convertTagList(in *openapi.TagList) unikornv1.TagList { return out } -func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unikornv1.Identity) error { - if identity.Spec.OpenStack.ServerGroupID != nil { +func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unikornv1.OpenstackIdentity) error { + if identity.Spec.ServerGroupID != nil { return nil } // Rescope to the user/project... - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.OpenStack.UserID, *identity.Spec.OpenStack.Password, *identity.Spec.OpenStack.ProjectID) + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.UserID, *identity.Spec.Password, *identity.Spec.ProjectID) computeService, err := NewComputeClient(ctx, providerClient, p.region.Spec.Openstack.Compute) if err != nil { @@ -573,11 +576,51 @@ func (p *Provider) createIdentityServerGroup(ctx context.Context, identity *unik return err } - identity.Spec.OpenStack.ServerGroupID = &result.ID + identity.Spec.ServerGroupID = &result.ID return nil } +func (p *Provider) GetOpenstackIdentity(ctx context.Context, identity *unikornv1.Identity) (*unikornv1.OpenstackIdentity, error) { + var result unikornv1.OpenstackIdentity + + if err := p.client.Get(ctx, client.ObjectKey{Namespace: identity.Namespace, Name: identity.Name}, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func (p *Provider) GetOrCreateOpenstackIdentity(ctx context.Context, identity *unikornv1.Identity) (*unikornv1.OpenstackIdentity, bool, error) { + create := false + + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) + if err != nil { + if !kerrors.IsNotFound(err) { + return nil, false, err + } + + openstackIdentity = &unikornv1.OpenstackIdentity{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: identity.Namespace, + Name: identity.Name, + Labels: map[string]string{ + constants.IdentityLabel: identity.Name, + }, + Annotations: identity.Annotations, + }, + } + + for k, v := range identity.Labels { + openstackIdentity.Labels[k] = v + } + + create = true + } + + return openstackIdentity, create, nil +} + // CreateIdentity creates a new identity for cloud infrastructure. func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Identity) error { identityService, err := p.identity(ctx) @@ -585,41 +628,60 @@ func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Ident return err } - if identity.Spec.OpenStack == nil { - identity.Spec.OpenStack = &unikornv1.IdentitySpecOpenStack{} + openstackIdentity, create, err := p.GetOrCreateOpenstackIdentity(ctx, identity) + if err != nil { + return err + } + + record := func() { + log := log.FromContext(ctx) + + if create { + if err := p.client.Create(ctx, openstackIdentity); err != nil { + log.Error(err, "failed to create openstack identity") + } + + return + } + + if err := p.client.Update(ctx, openstackIdentity); err != nil { + log.Error(err, "failed to update openstack identity") + } } + defer record() + // Every cluster has its own project to mitigate "nuances" in CAPO i.e. it's // totally broken when it comes to network aliasing. - if err := p.provisionProject(ctx, identityService, identity); err != nil { + if err := p.provisionProject(ctx, identityService, openstackIdentity); err != nil { return err } // You MUST provision a new user, if we rotate a password, any application credentials // hanging off it will stop working, i.e. doing that to the unikorn management user // will be pretty catastrophic for all clusters in the region. - if err := p.provisionUser(ctx, identityService, identity); err != nil { + if err := p.provisionUser(ctx, identityService, openstackIdentity); err != nil { return err } // Give the user only what permissions they need to provision a cluster and // manage it during its lifetime. - if err := p.provisionProjectRoles(ctx, identityService, identity); err != nil { + if err := p.provisionProjectRoles(ctx, identityService, openstackIdentity); err != nil { return err } // Always use application credentials, they are scoped to a single project and // cannot be used to break from that jail. - if err := p.provisionApplicationCredential(ctx, identity); err != nil { + if err := p.provisionApplicationCredential(ctx, openstackIdentity); err != nil { return err } - if err := p.createClientConfig(identity); err != nil { + if err := p.createClientConfig(openstackIdentity); err != nil { return err } // Add in any optional configuration. - if err := p.createIdentityServerGroup(ctx, identity); err != nil { + if err := p.createIdentityServerGroup(ctx, openstackIdentity); err != nil { return err } @@ -628,17 +690,26 @@ func (p *Provider) CreateIdentity(ctx context.Context, identity *unikornv1.Ident // DeleteIdentity cleans up an identity for cloud infrastructure. func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Identity) error { - // Rescope to the user/project... - if identity.Spec.OpenStack.UserID != nil && identity.Spec.OpenStack.ProjectID != nil { - providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *identity.Spec.OpenStack.UserID, *identity.Spec.OpenStack.Password, *identity.Spec.OpenStack.ProjectID) + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) + if err != nil { + if !kerrors.IsNotFound(err) { + return err + } + + return nil + } + + if openstackIdentity.Spec.UserID != nil && openstackIdentity.Spec.ProjectID != nil { + // Rescope to the user/project... + providerClient := NewPasswordProvider(p.region.Spec.Openstack.Endpoint, *openstackIdentity.Spec.UserID, *openstackIdentity.Spec.Password, *openstackIdentity.Spec.ProjectID) computeService, err := NewComputeClient(ctx, providerClient, p.region.Spec.Openstack.Compute) if err != nil { return err } - if identity.Spec.OpenStack.ServerGroupID != nil { - if err := computeService.DeleteServerGroup(ctx, *identity.Spec.OpenStack.ServerGroupID); err != nil { + if openstackIdentity.Spec.ServerGroupID != nil { + if err := computeService.DeleteServerGroup(ctx, *openstackIdentity.Spec.ServerGroupID); err != nil { return err } } @@ -649,29 +720,38 @@ func (p *Provider) DeleteIdentity(ctx context.Context, identity *unikornv1.Ident return err } - if identity.Spec.OpenStack.UserID != nil { - if err := identityService.DeleteUser(ctx, *identity.Spec.OpenStack.UserID); err != nil { + if openstackIdentity.Spec.UserID != nil { + if err := identityService.DeleteUser(ctx, *openstackIdentity.Spec.UserID); err != nil { return err } } - if identity.Spec.OpenStack.ProjectID != nil { - if err := identityService.DeleteProject(ctx, *identity.Spec.OpenStack.ProjectID); err != nil { + if openstackIdentity.Spec.ProjectID != nil { + if err := identityService.DeleteProject(ctx, *openstackIdentity.Spec.ProjectID); err != nil { return err } } + if err := p.client.Delete(ctx, openstackIdentity); err != nil { + return err + } + return nil } // CreatePhysicalNetwork creates a physical network for an identity. func (p *Provider) CreatePhysicalNetwork(ctx context.Context, identity *unikornv1.Identity, request *openapi.PhysicalNetworkWrite) (*unikornv1.PhysicalNetwork, error) { + openstackIdentity, err := p.GetOpenstackIdentity(ctx, identity) + if err != nil { + return nil, err + } + networkService, err := p.network(ctx) if err != nil { return nil, err } - vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "cluster-provider-network", *identity.Spec.OpenStack.ProjectID) + vlanID, providerNetwork, err := networkService.CreateVLANProviderNetwork(ctx, "cluster-provider-network", *openstackIdentity.Spec.ProjectID) if err != nil { return nil, err } diff --git a/pkg/provisioners/managers/identity/provisioner.go b/pkg/provisioners/managers/identity/provisioner.go index 4fe759a..980134e 100644 --- a/pkg/provisioners/managers/identity/provisioner.go +++ b/pkg/provisioners/managers/identity/provisioner.go @@ -21,13 +21,11 @@ import ( unikornv1core "github.com/unikorn-cloud/core/pkg/apis/unikorn/v1alpha1" coreclient "github.com/unikorn-cloud/core/pkg/client" + coremanager "github.com/unikorn-cloud/core/pkg/manager" "github.com/unikorn-cloud/core/pkg/provisioners" unikornv1 "github.com/unikorn-cloud/region/pkg/apis/unikorn/v1alpha1" "github.com/unikorn-cloud/region/pkg/constants" "github.com/unikorn-cloud/region/pkg/handler/region" - - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" ) // Provisioner encapsulates control plane provisioning. @@ -39,7 +37,7 @@ type Provisioner struct { } // New returns a new initialized provisioner object. -func New() provisioners.ManagerProvisioner { +func New(_ coremanager.ControllerOptions) provisioners.ManagerProvisioner { return &Provisioner{ identity: &unikornv1.Identity{}, } @@ -64,32 +62,7 @@ func (p *Provisioner) Provision(ctx context.Context) error { return err } - object := p.identity.DeepCopy() - - // Always try to update the resource as that carries state that allows us to - // be idempotent. - // TODO: most all of this mess goes away if we create a separate CR that can - // be updated by the controller independently of the identity. - update := func() { - log := log.FromContext(ctx) - - // This unfortunately will trigger another reconcile, but experience has told us - // that carrying infromation in the status is a bad idea, first as some backup - // solutions won't restore the status, and second we cannot re-geenrate things - // like passwords and secrets that are only available once. - if err := cli.Patch(ctx, object, client.MergeFrom(p.identity)); err != nil { - log.Error(err, "failed to update resource") - } - - // Update the object that the core controller refers to so that the resource - // version is up to date when it updates the status. This doesn't always work - // either! - p.identity = object - } - - defer update() - - if err := provider.CreateIdentity(ctx, object); err != nil { + if err := provider.CreateIdentity(ctx, p.identity); err != nil { return err } diff --git a/pkg/server/server.go b/pkg/server/server.go index 6d2fff5..8ff0aa9 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -31,6 +31,7 @@ import ( "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/trace" + coreclient "github.com/unikorn-cloud/core/pkg/client" coreapi "github.com/unikorn-cloud/core/pkg/openapi" "github.com/unikorn-cloud/core/pkg/server/middleware/audit" "github.com/unikorn-cloud/core/pkg/server/middleware/cors" @@ -58,18 +59,26 @@ type Server struct { // HandlerOptions sets options for the HTTP handler. HandlerOptions handler.Options + // ClientOptions are for generic TLS client options e.g. certificates. + ClientOptions coreclient.HTTPClientOptions + // IdentityOptions allow configuration of the authorization middleware. - IdentityOptions identityclient.Options + IdentityOptions *identityclient.Options // CORSOptions are for remote resource sharing. CORSOptions cors.Options } func (s *Server) AddFlags(goflags *flag.FlagSet, flags *pflag.FlagSet) { + if s.IdentityOptions == nil { + s.IdentityOptions = identityclient.NewOptions() + } + s.ZapOptions.BindFlags(goflags) s.Options.AddFlags(flags) s.HandlerOptions.AddFlags(flags) + s.ClientOptions.AddFlags(flags) s.IdentityOptions.AddFlags(flags) s.CORSOptions.AddFlags(flags) } @@ -143,7 +152,7 @@ func (s *Server) GetServer(client client.Client) (*http.Server, error) { router.NotFound(http.HandlerFunc(handler.NotFound)) router.MethodNotAllowed(http.HandlerFunc(handler.MethodNotAllowed)) - authorizer := openapimiddlewareremote.NewAuthorizer(client, s.Options.Namespace, &s.IdentityOptions) + authorizer := openapimiddlewareremote.NewAuthorizer(client, s.IdentityOptions, &s.ClientOptions) // Middleware specified here is applied to all requests post-routing. // NOTE: these are applied in reverse order!! @@ -156,7 +165,7 @@ func (s *Server) GetServer(client client.Client) (*http.Server, error) { }, } - identity := identityclient.New(client, s.Options.Namespace, &s.IdentityOptions) + identity := identityclient.New(client, s.IdentityOptions, &s.ClientOptions) handlerInterface, err := handler.New(client, s.Options.Namespace, &s.HandlerOptions, identity) if err != nil {