add namespace billinghistory query for costcenter (#3902)

* add namespace billinghistory query for costcenter
This commit is contained in:
Jiahui
2023-09-13 17:47:54 +08:00
committed by GitHub
parent 72b58791f0
commit 84b39f8183
18 changed files with 646 additions and 2 deletions
+9
View File
@@ -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"
@@ -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{})
}
@@ -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
@@ -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: []
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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
+26
View File
@@ -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:
@@ -0,0 +1,6 @@
apiVersion: account.sealos.io/v1
kind: NamespaceBillingHistory
metadata:
name: namespacebillinghistory-sample
spec:
# TODO(user): Add fields here
@@ -12,7 +12,6 @@ webhooks:
name: webhook-service
namespace: system
path: /validate-v1-sealos-cloud
timeoutSeconds: 10
failurePolicy: Ignore
name: debt.sealos.io
rules:
@@ -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)
}
@@ -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:
+9 -1
View File
@@ -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 {
+1
View File
@@ -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)
+38
View File
@@ -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(),
+24
View File
@@ -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)
}