From 84b39f8183316125e591e5b75eae3cfc69ef5480 Mon Sep 17 00:00:00 2001 From: Jiahui <4543bxy@gmail.com> Date: Wed, 13 Sep 2023 17:47:54 +0800 Subject: [PATCH] add namespace billinghistory query for costcenter (#3902) * add namespace billinghistory query for costcenter --- controllers/account/PROJECT | 9 ++ .../api/v1/namespacebillinghistory_types.go | 65 ++++++++++ .../account/api/v1/zz_generated.deepcopy.go | 99 +++++++++++++++ ...t.sealos.io_namespacebillinghistories.yaml | 77 ++++++++++++ .../account/config/crd/kustomization.yaml | 3 + ...njection_in_namespacebillinghistories.yaml | 7 ++ .../webhook_in_namespacebillinghistories.yaml | 16 +++ .../namespacebillinghistory_editor_role.yaml | 24 ++++ .../namespacebillinghistory_viewer_role.yaml | 20 +++ controllers/account/config/rbac/role.yaml | 26 ++++ .../account_v1_namespacebillinghistory.yaml | 6 + .../account/config/webhook/manifests.yaml | 1 - .../namespacebillinghistory_controller.go | 119 ++++++++++++++++++ .../account/deploy/manifests/deploy.yaml.tmpl | 103 +++++++++++++++ controllers/account/main.go | 10 +- controllers/pkg/database/interface.go | 1 + controllers/pkg/database/mongodb.go | 38 ++++++ controllers/pkg/database/mongodb_test.go | 24 ++++ 18 files changed, 646 insertions(+), 2 deletions(-) create mode 100644 controllers/account/api/v1/namespacebillinghistory_types.go create mode 100644 controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml create mode 100644 controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml create mode 100644 controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml create mode 100644 controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml create mode 100644 controllers/account/config/rbac/namespacebillinghistory_viewer_role.yaml create mode 100644 controllers/account/config/samples/account_v1_namespacebillinghistory.yaml create mode 100644 controllers/account/controllers/namespacebillinghistory_controller.go diff --git a/controllers/account/PROJECT b/controllers/account/PROJECT index 6d35907e6..d815bcc0e 100644 --- a/controllers/account/PROJECT +++ b/controllers/account/PROJECT @@ -83,4 +83,13 @@ resources: kind: Transfer path: github.com/labring/sealos/controllers/account/api/v1 version: v1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: sealos.io + group: account + kind: NamespaceBillingHistory + path: github.com/labring/sealos/controllers/account/api/v1 + version: v1 version: "3" diff --git a/controllers/account/api/v1/namespacebillinghistory_types.go b/controllers/account/api/v1/namespacebillinghistory_types.go new file mode 100644 index 000000000..cb0db06d8 --- /dev/null +++ b/controllers/account/api/v1/namespacebillinghistory_types.go @@ -0,0 +1,65 @@ +/* +Copyright 2023. + +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 v1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// NamespaceBillingHistorySpec defines the desired state of NamespaceBillingHistory +type NamespaceBillingHistorySpec struct { + StartTime metav1.Time `json:"startTime,omitempty"` + EndTime metav1.Time `json:"endTime,omitempty"` + Type Type `json:"type"` +} + +// NamespaceBillingHistoryStatus defines the observed state of NamespaceBillingHistory +type NamespaceBillingHistoryStatus struct { + // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster + // Important: Run "make" to regenerate code after modifying this file + NamespaceList []string `json:"namespaceList,omitempty"` + Status Status `json:"status,omitempty"` + Detail string `json:"detail,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status + +// NamespaceBillingHistory is the Schema for the namespacebillinghistories API +type NamespaceBillingHistory struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec NamespaceBillingHistorySpec `json:"spec,omitempty"` + Status NamespaceBillingHistoryStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// NamespaceBillingHistoryList contains a list of NamespaceBillingHistory +type NamespaceBillingHistoryList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []NamespaceBillingHistory `json:"items"` +} + +func init() { + SchemeBuilder.Register(&NamespaceBillingHistory{}, &NamespaceBillingHistoryList{}) +} diff --git a/controllers/account/api/v1/zz_generated.deepcopy.go b/controllers/account/api/v1/zz_generated.deepcopy.go index 1c39d014f..98f5e8eb1 100644 --- a/controllers/account/api/v1/zz_generated.deepcopy.go +++ b/controllers/account/api/v1/zz_generated.deepcopy.go @@ -199,6 +199,16 @@ func (in *AccountSpec) DeepCopy() *AccountSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AccountStatus) DeepCopyInto(out *AccountStatus) { *out = *in + if in.EncryptBalance != nil { + in, out := &in.EncryptBalance, &out.EncryptBalance + *out = new(string) + **out = **in + } + if in.EncryptDeductionBalance != nil { + in, out := &in.EncryptDeductionBalance, &out.EncryptDeductionBalance + *out = new(string) + **out = **in + } if in.ChargeList != nil { in, out := &in.ChargeList, &out.ChargeList *out = make([]Charge, len(*in)) @@ -464,6 +474,95 @@ func (in *DebtStatus) DeepCopy() *DebtStatus { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespaceBillingHistory) DeepCopyInto(out *NamespaceBillingHistory) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + out.Status = in.Status +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceBillingHistory. +func (in *NamespaceBillingHistory) DeepCopy() *NamespaceBillingHistory { + if in == nil { + return nil + } + out := new(NamespaceBillingHistory) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespaceBillingHistory) 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 *NamespaceBillingHistoryList) DeepCopyInto(out *NamespaceBillingHistoryList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]NamespaceBillingHistory, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceBillingHistoryList. +func (in *NamespaceBillingHistoryList) DeepCopy() *NamespaceBillingHistoryList { + if in == nil { + return nil + } + out := new(NamespaceBillingHistoryList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *NamespaceBillingHistoryList) 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 *NamespaceBillingHistorySpec) DeepCopyInto(out *NamespaceBillingHistorySpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceBillingHistorySpec. +func (in *NamespaceBillingHistorySpec) DeepCopy() *NamespaceBillingHistorySpec { + if in == nil { + return nil + } + out := new(NamespaceBillingHistorySpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NamespaceBillingHistoryStatus) DeepCopyInto(out *NamespaceBillingHistoryStatus) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NamespaceBillingHistoryStatus. +func (in *NamespaceBillingHistoryStatus) DeepCopy() *NamespaceBillingHistoryStatus { + if in == nil { + return nil + } + out := new(NamespaceBillingHistoryStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Payment) DeepCopyInto(out *Payment) { *out = *in diff --git a/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml b/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml new file mode 100644 index 000000000..fc20b39ab --- /dev/null +++ b/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml @@ -0,0 +1,77 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: namespacebillinghistories.account.sealos.io +spec: + group: account.sealos.io + names: + kind: NamespaceBillingHistory + listKind: NamespaceBillingHistoryList + plural: namespacebillinghistories + singular: namespacebillinghistory + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: NamespaceBillingHistory is the Schema for the namespacebillinghistories + API + 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: NamespaceBillingHistorySpec defines the desired state of + NamespaceBillingHistory + properties: + endTime: + format: date-time + type: string + startTime: + format: date-time + type: string + type: + type: integer + required: + - type + type: object + status: + description: NamespaceBillingHistoryStatus defines the observed state + of NamespaceBillingHistory + properties: + detail: + type: string + namespaceList: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + items: + type: string + type: array + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/controllers/account/config/crd/kustomization.yaml b/controllers/account/config/crd/kustomization.yaml index d61f3a4f7..18d9b5111 100644 --- a/controllers/account/config/crd/kustomization.yaml +++ b/controllers/account/config/crd/kustomization.yaml @@ -9,6 +9,7 @@ resources: - bases/account.sealos.io_billingrecordqueries.yaml - bases/account.sealos.io_pricequeries.yaml - bases/account.sealos.io_transfers.yaml +- bases/account.sealos.io_namespacebillinghistories.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -21,6 +22,7 @@ patchesStrategicMerge: #- patches/webhook_in_billingrecordqueries.yaml #- patches/webhook_in_pricequeries.yaml #- patches/webhook_in_transfers.yaml +#- patches/webhook_in_namespacebillinghistories.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -32,6 +34,7 @@ patchesStrategicMerge: #- patches/cainjection_in_billingrecordqueries.yaml #- patches/cainjection_in_pricequeries.yaml #- patches/cainjection_in_transfers.yaml +#- patches/cainjection_in_namespacebillinghistories.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml b/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml new file mode 100644 index 000000000..a2b66be19 --- /dev/null +++ b/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: namespacebillinghistories.account.sealos.io diff --git a/controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml b/controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml new file mode 100644 index 000000000..789a3d158 --- /dev/null +++ b/controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: namespacebillinghistories.account.sealos.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml b/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml new file mode 100644 index 000000000..c67751d02 --- /dev/null +++ b/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit namespacebillinghistories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: namespacebillinghistory-editor-role +rules: +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/status + verbs: + - get diff --git a/controllers/account/config/rbac/namespacebillinghistory_viewer_role.yaml b/controllers/account/config/rbac/namespacebillinghistory_viewer_role.yaml new file mode 100644 index 000000000..3e14d94ad --- /dev/null +++ b/controllers/account/config/rbac/namespacebillinghistory_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view namespacebillinghistories. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: namespacebillinghistory-viewer-role +rules: +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories + verbs: + - get + - list + - watch +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/status + verbs: + - get diff --git a/controllers/account/config/rbac/role.yaml b/controllers/account/config/rbac/role.yaml index 2027aacdb..794d071f6 100644 --- a/controllers/account/config/rbac/role.yaml +++ b/controllers/account/config/rbac/role.yaml @@ -125,6 +125,32 @@ rules: - get - patch - update +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/finalizers + verbs: + - update +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/status + verbs: + - get + - patch + - update - apiGroups: - account.sealos.io resources: diff --git a/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml b/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml new file mode 100644 index 000000000..023829ea7 --- /dev/null +++ b/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml @@ -0,0 +1,6 @@ +apiVersion: account.sealos.io/v1 +kind: NamespaceBillingHistory +metadata: + name: namespacebillinghistory-sample +spec: + # TODO(user): Add fields here diff --git a/controllers/account/config/webhook/manifests.yaml b/controllers/account/config/webhook/manifests.yaml index 321561071..c32d25066 100644 --- a/controllers/account/config/webhook/manifests.yaml +++ b/controllers/account/config/webhook/manifests.yaml @@ -12,7 +12,6 @@ webhooks: name: webhook-service namespace: system path: /validate-v1-sealos-cloud - timeoutSeconds: 10 failurePolicy: Ignore name: debt.sealos.io rules: diff --git a/controllers/account/controllers/namespacebillinghistory_controller.go b/controllers/account/controllers/namespacebillinghistory_controller.go new file mode 100644 index 000000000..f11f6d6ef --- /dev/null +++ b/controllers/account/controllers/namespacebillinghistory_controller.go @@ -0,0 +1,119 @@ +/* +Copyright 2023. + +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 controllers + +import ( + "context" + "fmt" + "os" + "time" + + userv1 "github.com/labring/sealos/controllers/user/api/v1" + + "github.com/go-logr/logr" + "github.com/labring/sealos/controllers/pkg/database" + "sigs.k8s.io/controller-runtime/pkg/log" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// NamespaceBillingHistoryReconciler reconciles a NamespaceBillingHistory object +type NamespaceBillingHistoryReconciler struct { + client.Client + Scheme *runtime.Scheme + Logger logr.Logger + MongoDBURI string +} + +//+kubebuilder:rbac:groups=account.sealos.io,resources=namespacebillinghistories,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=account.sealos.io,resources=namespacebillinghistories/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=account.sealos.io,resources=namespacebillinghistories/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the NamespaceBillingHistory object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile +func (r *NamespaceBillingHistoryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + dbCtx := context.Background() + dbClient, err := database.NewMongoDB(dbCtx, r.MongoDBURI) + if err != nil { + r.Logger.Error(err, "connect mongo client failed") + return ctrl.Result{Requeue: true}, err + } + defer func() { + err := dbClient.Disconnect(ctx) + if err != nil { + r.Logger.V(5).Info("disconnect mongo client failed", "err", err) + } + }() + + nsHistory := &accountv1.NamespaceBillingHistory{} + err = r.Get(ctx, req.NamespacedName, nsHistory) + if err == nil { + // delete after 3 minutes + if time.Since(nsHistory.CreationTimestamp.Time) > 3*time.Minute { + return ctrl.Result{}, r.Delete(ctx, nsHistory) + } + if err = r.reconcile(ctx, req, dbClient, nsHistory); err != nil { + r.Logger.Error(err, "reconcile failed") + nsHistory.Status.Status = accountv1.Failed + } + if err = r.Status().Update(ctx, nsHistory); err == nil { + // return after 4 minutes + return ctrl.Result{RequeueAfter: 4 * time.Minute}, nil + } + } + + return ctrl.Result{}, client.IgnoreNotFound(err) +} + +func (r *NamespaceBillingHistoryReconciler) reconcile(ctx context.Context, req ctrl.Request, dbClient database.Interface, nsHistory *accountv1.NamespaceBillingHistory) error { + user := &userv1.User{} + if err := r.Get(ctx, client.ObjectKey{Namespace: req.Namespace, Name: getUsername(req.Namespace)}, user); err != nil { + return fmt.Errorf("get user failed: %w", err) + } + owner, ok := user.GetAnnotations()[userv1.UserAnnotationOwnerKey] + if !ok { + return fmt.Errorf("user %s has no annotations %s", user.Name, userv1.UserLabelOwnerKey) + } + nsList, err := dbClient.GetBillingHistoryNamespaceList(&nsHistory.Spec, owner) + if err != nil { + return fmt.Errorf("get billing history namespace list failed: %w", err) + } + nsHistory.Status.NamespaceList = nsList + nsHistory.Status.Status = accountv1.Completed + return nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *NamespaceBillingHistoryReconciler) SetupWithManager(mgr ctrl.Manager) error { + if r.MongoDBURI = os.Getenv(database.MongoURI); r.MongoDBURI == "" { + return fmt.Errorf("env %s is empty", database.MongoURI) + } + r.Logger = log.Log.WithName("namespacebillinghistories-controller") + return ctrl.NewControllerManagedBy(mgr). + For(&accountv1.NamespaceBillingHistory{}). + Complete(r) +} diff --git a/controllers/account/deploy/manifests/deploy.yaml.tmpl b/controllers/account/deploy/manifests/deploy.yaml.tmpl index ae2c41927..d7d48fcb3 100644 --- a/controllers/account/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/account/deploy/manifests/deploy.yaml.tmpl @@ -388,6 +388,83 @@ status: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.8.0 + creationTimestamp: null + name: namespacebillinghistories.account.sealos.io +spec: + group: account.sealos.io + names: + kind: NamespaceBillingHistory + listKind: NamespaceBillingHistoryList + plural: namespacebillinghistories + singular: namespacebillinghistory + scope: Namespaced + versions: + - name: v1 + schema: + openAPIV3Schema: + description: NamespaceBillingHistory is the Schema for the namespacebillinghistories + API + 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: NamespaceBillingHistorySpec defines the desired state of + NamespaceBillingHistory + properties: + endTime: + format: date-time + type: string + startTime: + format: date-time + type: string + type: + type: integer + required: + - type + type: object + status: + description: NamespaceBillingHistoryStatus defines the observed state + of NamespaceBillingHistory + properties: + detail: + type: string + namespaceList: + description: 'INSERT ADDITIONAL STATUS FIELD - define observed state + of cluster Important: Run "make" to regenerate code after modifying + this file' + items: + type: string + type: array + status: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.8.0 @@ -765,6 +842,32 @@ rules: - get - patch - update +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/finalizers + verbs: + - update +- apiGroups: + - account.sealos.io + resources: + - namespacebillinghistories/status + verbs: + - get + - patch + - update - apiGroups: - account.sealos.io resources: diff --git a/controllers/account/main.go b/controllers/account/main.go index d2df112f8..70883fafe 100644 --- a/controllers/account/main.go +++ b/controllers/account/main.go @@ -46,8 +46,9 @@ import ( "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" - accountv1 "github.com/labring/sealos/controllers/account/api/v1" userv1 "github.com/labring/sealos/controllers/user/api/v1" + + accountv1 "github.com/labring/sealos/controllers/account/api/v1" //+kubebuilder:scaffold:imports ) @@ -190,6 +191,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Transfer") os.Exit(1) } + if err = (&controllers.NamespaceBillingHistoryReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "NamespaceBillingHistory") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/controllers/pkg/database/interface.go b/controllers/pkg/database/interface.go index 3a63c7a4a..7842e9374 100644 --- a/controllers/pkg/database/interface.go +++ b/controllers/pkg/database/interface.go @@ -26,6 +26,7 @@ type Interface interface { //InitDB() error GetMeteringOwnerTimeResult(queryTime time.Time, queryCategories, queryProperties []string) (*MeteringOwnerTimeResult, error) GetBillingLastUpdateTime(owner string, _type accountv1.Type) (bool, time.Time, error) + GetBillingHistoryNamespaceList(ns *accountv1.NamespaceBillingHistorySpec, owner string) ([]string, error) SaveBillingsWithAccountBalance(accountBalanceSpec *accountv1.AccountBalanceSpec) error QueryBillingRecords(billingRecordQuery *accountv1.BillingRecordQuery, owner string) error GetUpdateTimeForCategoryAndPropertyFromMetering(category string, property string) (time.Time, error) diff --git a/controllers/pkg/database/mongodb.go b/controllers/pkg/database/mongodb.go index fe5326845..41e2f99b6 100644 --- a/controllers/pkg/database/mongodb.go +++ b/controllers/pkg/database/mongodb.go @@ -100,6 +100,44 @@ func (m *MongoDB) GetBillingLastUpdateTime(owner string, _type accountv1.Type) ( return false, time.Time{}, fmt.Errorf("failed to convert time field to primitive.DateTime: %v", result["time"]) } +func (m *MongoDB) GetBillingHistoryNamespaceList(nsHistorySpec *accountv1.NamespaceBillingHistorySpec, owner string) ([]string, error) { + filter := bson.M{ + "owner": owner, + } + if nsHistorySpec.StartTime != nsHistorySpec.EndTime { + filter["time"] = bson.M{ + "$gte": nsHistorySpec.StartTime.Time.UTC(), + "$lte": nsHistorySpec.EndTime.Time.UTC(), + } + } + if nsHistorySpec.Type != -1 { + filter["type"] = nsHistorySpec.Type + } + + pipeline := mongo.Pipeline{ + {{"$match", filter}}, + {{"$group", bson.D{{"_id", nil}, {"namespaces", bson.D{{"$addToSet", "$namespace"}}}}}}, + } + + cur, err := m.getBillingCollection().Aggregate(context.Background(), pipeline) + if err != nil { + return nil, err + } + defer cur.Close(context.Background()) + + if !cur.Next(context.Background()) { + return []string{}, nil + } + + var result struct { + Namespaces []string `bson:"namespaces"` + } + if err := cur.Decode(&result); err != nil { + return nil, err + } + return result.Namespaces, nil +} + func (m *MongoDB) SaveBillingsWithAccountBalance(accountBalanceSpec *accountv1.AccountBalanceSpec) error { accountBalanceDoc := AccountBalanceSpecBSON{ Time: accountBalanceSpec.Time.Time.UTC(), diff --git a/controllers/pkg/database/mongodb_test.go b/controllers/pkg/database/mongodb_test.go index 1625bf9d9..1948a7a5d 100644 --- a/controllers/pkg/database/mongodb_test.go +++ b/controllers/pkg/database/mongodb_test.go @@ -417,3 +417,27 @@ func TestMongoDB_DropMonitorCollectionsOlderThan(t *testing.T) { t.Fatalf("failed to drop monitor collections older than 30 days: %v", err) } } + +func TestMongoDB_GetBillingHistoryNamespaceList(t *testing.T) { + dbCTX := context.Background() + m, err := NewMongoDB(dbCTX, os.Getenv("MONGODB_URI")) + if err != nil { + t.Errorf("failed to connect mongo: error = %v", err) + } + defer func() { + if err = m.Disconnect(dbCTX); err != nil { + t.Errorf("failed to disconnect mongo: error = %v", err) + } + }() + queryTime := time.Now().UTC() + billRecord := &accountv1.NamespaceBillingHistorySpec{ + StartTime: metav1.Time{Time: queryTime.Add(-time.Hour * 24 * 30)}, + EndTime: metav1.Time{Time: queryTime}, + Type: -1, + } + namespaceList, err := m.GetBillingHistoryNamespaceList(billRecord, "") + if err != nil { + t.Fatalf("failed to get billing history namespace list: %v", err) + } + t.Logf("namespaceList: %v", namespaceList) +}