diff --git a/apis/cloudian.go b/apis/cloudian.go index f02778d..0a6e16c 100644 --- a/apis/cloudian.go +++ b/apis/cloudian.go @@ -20,6 +20,7 @@ package apis import ( "k8s.io/apimachinery/pkg/runtime" + userv1alpha1 "github.com/statnett/provider-cloudian/apis/user/v1alpha1" cloudianv1alpha1 "github.com/statnett/provider-cloudian/apis/v1alpha1" ) @@ -27,6 +28,7 @@ func init() { // Register the types with the Scheme so the components can map objects to GroupVersionKinds and back AddToSchemes = append(AddToSchemes, cloudianv1alpha1.SchemeBuilder.AddToScheme, + userv1alpha1.SchemeBuilder.AddToScheme, ) } diff --git a/apis/user/user.go b/apis/user/user.go new file mode 100644 index 0000000..a2908d6 --- /dev/null +++ b/apis/user/user.go @@ -0,0 +1,18 @@ +/* +Copyright 2022 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package user contains group user API versions +package user diff --git a/apis/user/v1alpha1/doc.go b/apis/user/v1alpha1/doc.go new file mode 100644 index 0000000..dc5d3ae --- /dev/null +++ b/apis/user/v1alpha1/doc.go @@ -0,0 +1,17 @@ +/* +Copyright 2022 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 diff --git a/apis/user/v1alpha1/group_types.go b/apis/user/v1alpha1/group_types.go new file mode 100644 index 0000000..2dfc908 --- /dev/null +++ b/apis/user/v1alpha1/group_types.go @@ -0,0 +1,86 @@ +/* +Copyright 2022 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package v1alpha1 + +import ( + "reflect" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + + xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" +) + +// GroupParameters are the configurable fields of a Group. +type GroupParameters struct { + ConfigurableField string `json:"configurableField"` +} + +// GroupObservation are the observable fields of a Group. +type GroupObservation struct { + ObservableField string `json:"observableField,omitempty"` +} + +// A GroupSpec defines the desired state of a Group. +type GroupSpec struct { + xpv1.ResourceSpec `json:",inline"` + ForProvider GroupParameters `json:"forProvider"` +} + +// A GroupStatus represents the observed state of a Group. +type GroupStatus struct { + xpv1.ResourceStatus `json:",inline"` + AtProvider GroupObservation `json:"atProvider,omitempty"` +} + +// +kubebuilder:object:root=true + +// A Group is an example API type. +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status" +// +kubebuilder:printcolumn:name="SYNCED",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status" +// +kubebuilder:printcolumn:name="EXTERNAL-NAME",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" +// +kubebuilder:subresource:status +// +kubebuilder:resource:scope=Cluster,categories={crossplane,managed,cloudian} +type Group struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GroupSpec `json:"spec"` + Status GroupStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// GroupList contains a list of Group +type GroupList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Group `json:"items"` +} + +// Group type metadata. +var ( + GroupKind = reflect.TypeOf(Group{}).Name() + GroupGroupKind = schema.GroupKind{Group: MetadataGroup, Kind: GroupKind}.String() + GroupKindAPIVersion = GroupKind + "." + SchemeGroupVersion.String() + GroupGroupVersionKind = SchemeGroupVersion.WithKind(GroupKind) +) + +func init() { + SchemeBuilder.Register(&Group{}, &GroupList{}) +} diff --git a/apis/user/v1alpha1/groupversion_info.go b/apis/user/v1alpha1/groupversion_info.go new file mode 100644 index 0000000..ff94135 --- /dev/null +++ b/apis/user/v1alpha1/groupversion_info.go @@ -0,0 +1,40 @@ +/* +Copyright 2020 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package v1alpha1 contains the v1alpha1 group Sample resources of the Cloudian provider. +// +kubebuilder:object:generate=true +// +groupName=user.cloudian.crossplane.io +// +versionName=v1alpha1 +package v1alpha1 + +import ( + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/scheme" +) + +// Package type metadata. +const ( + MetadataGroup = "user.cloudian.crossplane.io" + Version = "v1alpha1" +) + +var ( + // SchemeGroupVersion is group version used to register these objects + SchemeGroupVersion = schema.GroupVersion{Group: MetadataGroup, Version: Version} + + // SchemeBuilder is used to add go types to the GroupVersionKind scheme + SchemeBuilder = &scheme.Builder{GroupVersion: SchemeGroupVersion} +) diff --git a/apis/user/v1alpha1/zz_generated.deepcopy.go b/apis/user/v1alpha1/zz_generated.deepcopy.go new file mode 100644 index 0000000..38f5958 --- /dev/null +++ b/apis/user/v1alpha1/zz_generated.deepcopy.go @@ -0,0 +1,148 @@ +//go:build !ignore_autogenerated + +/* +Copyright 2020 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Code generated by controller-gen. DO NOT EDIT. + +package v1alpha1 + +import ( + runtime "k8s.io/apimachinery/pkg/runtime" +) + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Group) DeepCopyInto(out *Group) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Group. +func (in *Group) DeepCopy() *Group { + if in == nil { + return nil + } + out := new(Group) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Group) 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 *GroupList) DeepCopyInto(out *GroupList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Group, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupList. +func (in *GroupList) DeepCopy() *GroupList { + if in == nil { + return nil + } + out := new(GroupList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GroupList) 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 *GroupObservation) DeepCopyInto(out *GroupObservation) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupObservation. +func (in *GroupObservation) DeepCopy() *GroupObservation { + if in == nil { + return nil + } + out := new(GroupObservation) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupParameters) DeepCopyInto(out *GroupParameters) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupParameters. +func (in *GroupParameters) DeepCopy() *GroupParameters { + if in == nil { + return nil + } + out := new(GroupParameters) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupSpec) DeepCopyInto(out *GroupSpec) { + *out = *in + in.ResourceSpec.DeepCopyInto(&out.ResourceSpec) + out.ForProvider = in.ForProvider +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupSpec. +func (in *GroupSpec) DeepCopy() *GroupSpec { + if in == nil { + return nil + } + out := new(GroupSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GroupStatus) DeepCopyInto(out *GroupStatus) { + *out = *in + in.ResourceStatus.DeepCopyInto(&out.ResourceStatus) + out.AtProvider = in.AtProvider +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GroupStatus. +func (in *GroupStatus) DeepCopy() *GroupStatus { + if in == nil { + return nil + } + out := new(GroupStatus) + in.DeepCopyInto(out) + return out +} diff --git a/apis/user/v1alpha1/zz_generated.managed.go b/apis/user/v1alpha1/zz_generated.managed.go new file mode 100644 index 0000000..f8294a6 --- /dev/null +++ b/apis/user/v1alpha1/zz_generated.managed.go @@ -0,0 +1,80 @@ +/* +Copyright 2020 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1" + +// GetCondition of this Group. +func (mg *Group) GetCondition(ct xpv1.ConditionType) xpv1.Condition { + return mg.Status.GetCondition(ct) +} + +// GetDeletionPolicy of this Group. +func (mg *Group) GetDeletionPolicy() xpv1.DeletionPolicy { + return mg.Spec.DeletionPolicy +} + +// GetManagementPolicies of this Group. +func (mg *Group) GetManagementPolicies() xpv1.ManagementPolicies { + return mg.Spec.ManagementPolicies +} + +// GetProviderConfigReference of this Group. +func (mg *Group) GetProviderConfigReference() *xpv1.Reference { + return mg.Spec.ProviderConfigReference +} + +// GetPublishConnectionDetailsTo of this Group. +func (mg *Group) GetPublishConnectionDetailsTo() *xpv1.PublishConnectionDetailsTo { + return mg.Spec.PublishConnectionDetailsTo +} + +// GetWriteConnectionSecretToReference of this Group. +func (mg *Group) GetWriteConnectionSecretToReference() *xpv1.SecretReference { + return mg.Spec.WriteConnectionSecretToReference +} + +// SetConditions of this Group. +func (mg *Group) SetConditions(c ...xpv1.Condition) { + mg.Status.SetConditions(c...) +} + +// SetDeletionPolicy of this Group. +func (mg *Group) SetDeletionPolicy(r xpv1.DeletionPolicy) { + mg.Spec.DeletionPolicy = r +} + +// SetManagementPolicies of this Group. +func (mg *Group) SetManagementPolicies(r xpv1.ManagementPolicies) { + mg.Spec.ManagementPolicies = r +} + +// SetProviderConfigReference of this Group. +func (mg *Group) SetProviderConfigReference(r *xpv1.Reference) { + mg.Spec.ProviderConfigReference = r +} + +// SetPublishConnectionDetailsTo of this Group. +func (mg *Group) SetPublishConnectionDetailsTo(r *xpv1.PublishConnectionDetailsTo) { + mg.Spec.PublishConnectionDetailsTo = r +} + +// SetWriteConnectionSecretToReference of this Group. +func (mg *Group) SetWriteConnectionSecretToReference(r *xpv1.SecretReference) { + mg.Spec.WriteConnectionSecretToReference = r +} diff --git a/apis/user/v1alpha1/zz_generated.managedlist.go b/apis/user/v1alpha1/zz_generated.managedlist.go new file mode 100644 index 0000000..2bf853c --- /dev/null +++ b/apis/user/v1alpha1/zz_generated.managedlist.go @@ -0,0 +1,29 @@ +/* +Copyright 2020 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +// Code generated by angryjet. DO NOT EDIT. + +package v1alpha1 + +import resource "github.com/crossplane/crossplane-runtime/pkg/resource" + +// GetItems of this GroupList. +func (l *GroupList) GetItems() []resource.Managed { + items := make([]resource.Managed, len(l.Items)) + for i := range l.Items { + items[i] = &l.Items[i] + } + return items +} diff --git a/go.mod b/go.mod index efb7db2..ca8ae48 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,8 @@ go 1.21 require ( github.com/crossplane/crossplane-runtime v1.16.0 github.com/crossplane/crossplane-tools v0.0.0-20230925130601-628280f8bf79 + github.com/google/go-cmp v0.6.0 + github.com/pkg/errors v0.9.1 gopkg.in/alecthomas/kingpin.v2 v2.2.6 k8s.io/apimachinery v0.29.2 k8s.io/client-go v0.29.2 @@ -36,7 +38,6 @@ require ( github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/gnostic-models v0.6.8 // indirect - github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/uuid v1.4.0 // indirect github.com/imdario/mergo v0.3.16 // indirect @@ -50,7 +51,6 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect - github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_golang v1.18.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.45.0 // indirect @@ -71,6 +71,8 @@ require ( golang.org/x/tools v0.17.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/appengine v1.6.8 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f // indirect + google.golang.org/grpc v1.61.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index 90869ec..4df3d55 100644 --- a/go.sum +++ b/go.sum @@ -213,6 +213,10 @@ gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f h1:ultW7fxlIvee4HYrtnaRPon9HpEgFk5zYpmfMgtKB5I= +google.golang.org/genproto/googleapis/rpc v0.0.0-20231120223509-83a465c0220f/go.mod h1:L9KNLi232K1/xB6f7AlSX692koaRnKaWSR0stBki0Yc= +google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= +google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= diff --git a/internal/controller/cloudian.go b/internal/controller/cloudian.go index 718da3f..f270bfa 100644 --- a/internal/controller/cloudian.go +++ b/internal/controller/cloudian.go @@ -21,6 +21,7 @@ import ( ctrl "sigs.k8s.io/controller-runtime" "github.com/statnett/provider-cloudian/internal/controller/config" + "github.com/statnett/provider-cloudian/internal/controller/group" ) // Setup creates all Cloudian controllers with the supplied logger and adds them to @@ -28,6 +29,7 @@ import ( func Setup(mgr ctrl.Manager, o controller.Options) error { for _, setup := range []func(ctrl.Manager, controller.Options) error{ config.Setup, + group.Setup, } { if err := setup(mgr, o); err != nil { return err diff --git a/internal/controller/group/group.go b/internal/controller/group/group.go new file mode 100644 index 0000000..bcc50ac --- /dev/null +++ b/internal/controller/group/group.go @@ -0,0 +1,199 @@ +/* +Copyright 2022 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package group + +import ( + "context" + "fmt" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/crossplane/crossplane-runtime/pkg/connection" + "github.com/crossplane/crossplane-runtime/pkg/controller" + "github.com/crossplane/crossplane-runtime/pkg/event" + "github.com/crossplane/crossplane-runtime/pkg/ratelimiter" + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + + "github.com/statnett/provider-cloudian/apis/user/v1alpha1" + apisv1alpha1 "github.com/statnett/provider-cloudian/apis/v1alpha1" + "github.com/statnett/provider-cloudian/internal/features" +) + +const ( + errNotGroup = "managed resource is not a Group custom resource" + errTrackPCUsage = "cannot track ProviderConfig usage" + errGetPC = "cannot get ProviderConfig" + errGetCreds = "cannot get credentials" + + errNewClient = "cannot create new Service" +) + +// A NoOpService does nothing. +type NoOpService struct{} + +var ( + newNoOpService = func(_ []byte) (interface{}, error) { return &NoOpService{}, nil } +) + +// Setup adds a controller that reconciles Group managed resources. +func Setup(mgr ctrl.Manager, o controller.Options) error { + name := managed.ControllerName(v1alpha1.GroupGroupKind) + + cps := []managed.ConnectionPublisher{managed.NewAPISecretPublisher(mgr.GetClient(), mgr.GetScheme())} + if o.Features.Enabled(features.EnableAlphaExternalSecretStores) { + cps = append(cps, connection.NewDetailsManager(mgr.GetClient(), apisv1alpha1.StoreConfigGroupVersionKind)) + } + + r := managed.NewReconciler(mgr, + resource.ManagedKind(v1alpha1.GroupGroupVersionKind), + managed.WithExternalConnecter(&connector{ + kube: mgr.GetClient(), + usage: resource.NewProviderConfigUsageTracker(mgr.GetClient(), &apisv1alpha1.ProviderConfigUsage{}), + newServiceFn: newNoOpService}), + managed.WithLogger(o.Logger.WithValues("controller", name)), + managed.WithPollInterval(o.PollInterval), + managed.WithRecorder(event.NewAPIRecorder(mgr.GetEventRecorderFor(name))), + managed.WithConnectionPublishers(cps...)) + + return ctrl.NewControllerManagedBy(mgr). + Named(name). + WithOptions(o.ForControllerRuntime()). + WithEventFilter(resource.DesiredStateChanged()). + For(&v1alpha1.Group{}). + Complete(ratelimiter.NewReconciler(name, r, o.GlobalRateLimiter)) +} + +// A connector is expected to produce an ExternalClient when its Connect method +// is called. +type connector struct { + kube client.Client + usage resource.Tracker + newServiceFn func(creds []byte) (interface{}, error) +} + +// Connect typically produces an ExternalClient by: +// 1. Tracking that the managed resource is using a ProviderConfig. +// 2. Getting the managed resource's ProviderConfig. +// 3. Getting the credentials specified by the ProviderConfig. +// 4. Using the credentials to form a client. +func (c *connector) Connect(ctx context.Context, mg resource.Managed) (managed.ExternalClient, error) { + cr, ok := mg.(*v1alpha1.Group) + if !ok { + return nil, errors.New(errNotGroup) + } + + if err := c.usage.Track(ctx, mg); err != nil { + return nil, errors.Wrap(err, errTrackPCUsage) + } + + pc := &apisv1alpha1.ProviderConfig{} + if err := c.kube.Get(ctx, types.NamespacedName{Name: cr.GetProviderConfigReference().Name}, pc); err != nil { + return nil, errors.Wrap(err, errGetPC) + } + + cd := pc.Spec.Credentials + data, err := resource.CommonCredentialExtractor(ctx, cd.Source, c.kube, cd.CommonCredentialSelectors) + if err != nil { + return nil, errors.Wrap(err, errGetCreds) + } + + svc, err := c.newServiceFn(data) + if err != nil { + return nil, errors.Wrap(err, errNewClient) + } + + return &external{service: svc}, nil +} + +// An ExternalClient observes, then either creates, updates, or deletes an +// external resource to ensure it reflects the managed resource's desired state. +type external struct { + // A 'client' used to connect to the external resource API. In practice this + // would be something like an AWS SDK client. + service interface{} +} + +func (c *external) Observe(ctx context.Context, mg resource.Managed) (managed.ExternalObservation, error) { + cr, ok := mg.(*v1alpha1.Group) + if !ok { + return managed.ExternalObservation{}, errors.New(errNotGroup) + } + + // These fmt statements should be removed in the real implementation. + fmt.Printf("Observing: %+v", cr) + + return managed.ExternalObservation{ + // Return false when the external resource does not exist. This lets + // the managed resource reconciler know that it needs to call Create to + // (re)create the resource, or that it has successfully been deleted. + ResourceExists: true, + + // Return false when the external resource exists, but it not up to date + // with the desired managed resource state. This lets the managed + // resource reconciler know that it needs to call Update. + ResourceUpToDate: true, + + // Return any details that may be required to connect to the external + // resource. These will be stored as the connection secret. + ConnectionDetails: managed.ConnectionDetails{}, + }, nil +} + +func (c *external) Create(ctx context.Context, mg resource.Managed) (managed.ExternalCreation, error) { + cr, ok := mg.(*v1alpha1.Group) + if !ok { + return managed.ExternalCreation{}, errors.New(errNotGroup) + } + + fmt.Printf("Creating: %+v", cr) + + return managed.ExternalCreation{ + // Optionally return any details that may be required to connect to the + // external resource. These will be stored as the connection secret. + ConnectionDetails: managed.ConnectionDetails{}, + }, nil +} + +func (c *external) Update(ctx context.Context, mg resource.Managed) (managed.ExternalUpdate, error) { + cr, ok := mg.(*v1alpha1.Group) + if !ok { + return managed.ExternalUpdate{}, errors.New(errNotGroup) + } + + fmt.Printf("Updating: %+v", cr) + + return managed.ExternalUpdate{ + // Optionally return any details that may be required to connect to the + // external resource. These will be stored as the connection secret. + ConnectionDetails: managed.ConnectionDetails{}, + }, nil +} + +func (c *external) Delete(ctx context.Context, mg resource.Managed) error { + cr, ok := mg.(*v1alpha1.Group) + if !ok { + return errors.New(errNotGroup) + } + + fmt.Printf("Deleting: %+v", cr) + + return nil +} diff --git a/internal/controller/group/group_test.go b/internal/controller/group/group_test.go new file mode 100644 index 0000000..bca54b0 --- /dev/null +++ b/internal/controller/group/group_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2022 The Crossplane Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package group + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + + "github.com/crossplane/crossplane-runtime/pkg/reconciler/managed" + "github.com/crossplane/crossplane-runtime/pkg/resource" + "github.com/crossplane/crossplane-runtime/pkg/test" +) + +// Unlike many Kubernetes projects Crossplane does not use third party testing +// libraries, per the common Go test review comments. Crossplane encourages the +// use of table driven unit tests. The tests of the crossplane-runtime project +// are representative of the testing style Crossplane encourages. +// +// https://github.com/golang/go/wiki/TestComments +// https://github.com/crossplane/crossplane/blob/master/CONTRIBUTING.md#contributing-code + +func TestObserve(t *testing.T) { + type fields struct { + service interface{} + } + + type args struct { + ctx context.Context + mg resource.Managed + } + + type want struct { + o managed.ExternalObservation + err error + } + + cases := map[string]struct { + reason string + fields fields + args args + want want + }{ + // TODO: Add test cases. + } + + for name, tc := range cases { + t.Run(name, func(t *testing.T) { + e := external{service: tc.fields.service} + got, err := e.Observe(tc.args.ctx, tc.args.mg) + if diff := cmp.Diff(tc.want.err, err, test.EquateErrors()); diff != "" { + t.Errorf("\n%s\ne.Observe(...): -want error, +got error:\n%s\n", tc.reason, diff) + } + if diff := cmp.Diff(tc.want.o, got); diff != "" { + t.Errorf("\n%s\ne.Observe(...): -want, +got:\n%s\n", tc.reason, diff) + } + }) + } +} diff --git a/package/crds/user.cloudian.crossplane.io_groups.yaml b/package/crds/user.cloudian.crossplane.io_groups.yaml new file mode 100644 index 0000000..836d8b9 --- /dev/null +++ b/package/crds/user.cloudian.crossplane.io_groups.yaml @@ -0,0 +1,317 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.14.0 + name: groups.user.cloudian.crossplane.io +spec: + group: user.cloudian.crossplane.io + names: + categories: + - crossplane + - managed + - cloudian + kind: Group + listKind: GroupList + plural: groups + singular: group + scope: Cluster + versions: + - additionalPrinterColumns: + - jsonPath: .status.conditions[?(@.type=='Ready')].status + name: READY + type: string + - jsonPath: .status.conditions[?(@.type=='Synced')].status + name: SYNCED + type: string + - jsonPath: .metadata.annotations.crossplane\.io/external-name + name: EXTERNAL-NAME + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: A Group is an example API type. + 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: + description: A GroupSpec defines the desired state of a Group. + properties: + deletionPolicy: + default: Delete + description: |- + DeletionPolicy specifies what will happen to the underlying external + when this managed resource is deleted - either "Delete" or "Orphan" the + external resource. + This field is planned to be deprecated in favor of the ManagementPolicies + field in a future release. Currently, both could be set independently and + non-default values would be honored if the feature flag is enabled. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + enum: + - Orphan + - Delete + type: string + forProvider: + description: GroupParameters are the configurable fields of a Group. + properties: + configurableField: + type: string + required: + - configurableField + type: object + managementPolicies: + default: + - '*' + description: |- + THIS IS A BETA FIELD. It is on by default but can be opted out + through a Crossplane feature flag. + ManagementPolicies specify the array of actions Crossplane is allowed to + take on the managed and external resources. + This field is planned to replace the DeletionPolicy field in a future + release. Currently, both could be set independently and non-default + values would be honored if the feature flag is enabled. If both are + custom, the DeletionPolicy field will be ignored. + See the design doc for more information: https://github.com/crossplane/crossplane/blob/499895a25d1a1a0ba1604944ef98ac7a1a71f197/design/design-doc-observe-only-resources.md?plain=1#L223 + and this one: https://github.com/crossplane/crossplane/blob/444267e84783136daa93568b364a5f01228cacbe/design/one-pager-ignore-changes.md + items: + description: |- + A ManagementAction represents an action that the Crossplane controllers + can take on an external resource. + enum: + - Observe + - Create + - Update + - Delete + - LateInitialize + - '*' + type: string + type: array + providerConfigRef: + default: + name: default + description: |- + ProviderConfigReference specifies how the provider that will be used to + create, observe, update, and delete this managed resource should be + configured. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + publishConnectionDetailsTo: + description: |- + PublishConnectionDetailsTo specifies the connection secret config which + contains a name, metadata and a reference to secret store config to + which any connection details for this managed resource should be written. + Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + properties: + configRef: + default: + name: default + description: |- + SecretStoreConfigRef specifies which secret store config should be used + for this ConnectionSecret. + properties: + name: + description: Name of the referenced object. + type: string + policy: + description: Policies for referencing. + properties: + resolution: + default: Required + description: |- + Resolution specifies whether resolution of this reference is required. + The default is 'Required', which means the reconcile will fail if the + reference cannot be resolved. 'Optional' means this reference will be + a no-op if it cannot be resolved. + enum: + - Required + - Optional + type: string + resolve: + description: |- + Resolve specifies when this reference should be resolved. The default + is 'IfNotPresent', which will attempt to resolve the reference only when + the corresponding field is not present. Use 'Always' to resolve the + reference on every reconcile. + enum: + - Always + - IfNotPresent + type: string + type: object + required: + - name + type: object + metadata: + description: Metadata is the metadata for connection secret. + properties: + annotations: + additionalProperties: + type: string + description: |- + Annotations are the annotations to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.annotations". + - It is up to Secret Store implementation for others store types. + type: object + labels: + additionalProperties: + type: string + description: |- + Labels are the labels/tags to be added to connection secret. + - For Kubernetes secrets, this will be used as "metadata.labels". + - It is up to Secret Store implementation for others store types. + type: object + type: + description: |- + Type is the SecretType for the connection secret. + - Only valid for Kubernetes Secret Stores. + type: string + type: object + name: + description: Name is the name of the connection secret. + type: string + required: + - name + type: object + writeConnectionSecretToRef: + description: |- + WriteConnectionSecretToReference specifies the namespace and name of a + Secret to which any connection details for this managed resource should + be written. Connection details frequently include the endpoint, username, + and password required to connect to the managed resource. + This field is planned to be replaced in a future release in favor of + PublishConnectionDetailsTo. Currently, both could be set independently + and connection details would be published to both without affecting + each other. + properties: + name: + description: Name of the secret. + type: string + namespace: + description: Namespace of the secret. + type: string + required: + - name + - namespace + type: object + required: + - forProvider + type: object + status: + description: A GroupStatus represents the observed state of a Group. + properties: + atProvider: + description: GroupObservation are the observable fields of a Group. + properties: + observableField: + type: string + type: object + conditions: + description: Conditions of the resource. + items: + description: A Condition that may apply to a resource. + properties: + lastTransitionTime: + description: |- + LastTransitionTime is the last time this condition transitioned from one + status to another. + format: date-time + type: string + message: + description: |- + A Message containing details about this condition's last transition from + one status to another, if any. + type: string + observedGeneration: + description: |- + ObservedGeneration represents the .metadata.generation that the condition was set based upon. + For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date + with respect to the current state of the instance. + format: int64 + type: integer + reason: + description: A Reason for this condition's last transition from + one status to another. + type: string + status: + description: Status of this condition; is it currently True, + False, or Unknown? + type: string + type: + description: |- + Type of this condition. At most one of each condition type may apply to + a resource at any point in time. + type: string + required: + - lastTransitionTime + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + observedGeneration: + description: |- + ObservedGeneration is the latest metadata.generation + which resulted in either a ready state, or stalled due to error + it can not recover from without human intervention. + format: int64 + type: integer + type: object + required: + - spec + type: object + served: true + storage: true + subresources: + status: {}