Skip to content

Commit

Permalink
Feat/cost service (#4391)
Browse files Browse the repository at this point in the history
* init account service

* rebase

* add properties query

# Conflicts:
#	service/go.work.sum

* add GetCostAmount query api

* format && add account service workflow

* add swagger api docment for account service

* rebase upstream
  • Loading branch information
bxy4543 committed Dec 13, 2023
1 parent ad77db0 commit 4bea4ea
Show file tree
Hide file tree
Showing 36 changed files with 2,458 additions and 213 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/services.yml
Expand Up @@ -86,7 +86,7 @@ jobs:
strategy:
matrix:
## TODO: add more modules
module: [ database, pay ]
module: [ database, pay, account ]
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down Expand Up @@ -181,7 +181,7 @@ jobs:
strategy:
matrix:
## TODO: add more modules
module: [ database, pay ]
module: [ database, pay, account ]
steps:
- name: Checkout
uses: actions/checkout@v3
Expand Down
6 changes: 4 additions & 2 deletions controllers/account/api/v1/account_types.go
Expand Up @@ -18,6 +18,8 @@ package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/labring/sealos/controllers/pkg/common"
)

const (
Expand All @@ -26,12 +28,12 @@ const (

type (
Status string
Type int
Type common.Type
)

const (
// Consumption 消费
Consumption Type = iota
Consumption common.Type = iota
// Recharge 充值
Recharge
TransferIn
Expand Down
14 changes: 8 additions & 6 deletions controllers/account/api/v1/billingrecordquery_types.go
Expand Up @@ -18,6 +18,8 @@ package v1

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/labring/sealos/controllers/pkg/common"
)

// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN!
Expand Down Expand Up @@ -55,12 +57,12 @@ type BillingRecordQueryItem struct {
}

type BillingRecordQueryItemInline struct {
Name string `json:"name,omitempty" bson:"name,omitempty"`
OrderID string `json:"order_id" bson:"order_id"`
Namespace string `json:"namespace,omitempty" bson:"namespace,omitempty"`
Type Type `json:"type" bson:"type"`
AppType string `json:"appType,omitempty" bson:"appType,omitempty"`
Costs Costs `json:"costs,omitempty" bson:"costs,omitempty"`
Name string `json:"name,omitempty" bson:"name,omitempty"`
OrderID string `json:"order_id" bson:"order_id"`
Namespace string `json:"namespace,omitempty" bson:"namespace,omitempty"`
Type common.Type `json:"type" bson:"type"`
AppType string `json:"appType,omitempty" bson:"appType,omitempty"`
Costs Costs `json:"costs,omitempty" bson:"costs,omitempty"`
//Amount = PaymentAmount + GiftAmount
Amount int64 `json:"amount,omitempty" bson:"amount"`
// when Type = Recharge, PaymentAmount is the amount of recharge
Expand Down
6 changes: 4 additions & 2 deletions controllers/account/controllers/transfer_controller.go
Expand Up @@ -23,6 +23,8 @@ import (
"strconv"
"time"

"github.com/labring/sealos/controllers/pkg/common"

"github.com/labring/sealos/controllers/pkg/resources"

"github.com/labring/sealos/controllers/pkg/database"
Expand Down Expand Up @@ -206,12 +208,12 @@ const (
TransferOutNotification = `You have a new transfer to %s, amount: %d`
)

var transferNotification = map[accountv1.Type]string{
var transferNotification = map[common.Type]string{
accountv1.TransferIn: TransferInNotification,
accountv1.TransferOut: TransferOutNotification,
}

func (r *TransferReconciler) sendNotice(ctx context.Context, namespace string, user string, amount int64, _type accountv1.Type) error {
func (r *TransferReconciler) sendNotice(ctx context.Context, namespace string, user string, amount int64, _type common.Type) error {
now := time.Now().UTC().Unix()
ntf := v1.Notification{
ObjectMeta: metav1.ObjectMeta{
Expand Down
17 changes: 17 additions & 0 deletions controllers/pkg/common/account.go
@@ -0,0 +1,17 @@
// Copyright © 2023 sealos.
//
// 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 common

type Type int
6 changes: 4 additions & 2 deletions controllers/pkg/database/interface.go
Expand Up @@ -20,6 +20,8 @@ import (

"github.com/labring/sealos/controllers/pkg/types"

"github.com/labring/sealos/controllers/pkg/common"

accountv1 "github.com/labring/sealos/controllers/account/api/v1"
"github.com/labring/sealos/controllers/pkg/resources"
)
Expand All @@ -35,7 +37,7 @@ type Auth interface {

type Account interface {
//InitDB() error
GetBillingLastUpdateTime(owner string, _type accountv1.Type) (bool, time.Time, error)
GetBillingLastUpdateTime(owner string, _type common.Type) (bool, time.Time, error)
GetBillingHistoryNamespaceList(ns *accountv1.NamespaceBillingHistorySpec, owner string) ([]string, error)
GetBillingHistoryNamespaces(startTime, endTime *time.Time, billType int, owner string) ([]string, error)
SaveBillings(billing ...*resources.Billing) error
Expand All @@ -46,7 +48,7 @@ type Account interface {
GetAllPricesMap() (map[string]resources.Price, error)
InitDefaultPropertyTypeLS() error
SavePropertyTypes(types []resources.PropertyType) error
GetBillingCount(accountType accountv1.Type, startTime, endTime time.Time) (count, amount int64, err error)
GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error)
GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, namespaces []string, owner string) (orderID []string, amount int64, err error)
InsertMonitor(ctx context.Context, monitors ...*resources.Monitor) error
DropMonitorCollectionsOlderThan(days int) error
Expand Down
5 changes: 3 additions & 2 deletions controllers/pkg/database/mongo/account.go
Expand Up @@ -22,6 +22,7 @@ import (
"strings"
"time"

"github.com/labring/sealos/controllers/pkg/common"
"github.com/labring/sealos/controllers/pkg/database"

gonanoid "github.com/matoous/go-nanoid/v2"
Expand Down Expand Up @@ -80,7 +81,7 @@ func (m *mongoDB) Disconnect(ctx context.Context) error {
return m.Client.Disconnect(ctx)
}

func (m *mongoDB) GetBillingLastUpdateTime(owner string, _type accountv1.Type) (bool, time.Time, error) {
func (m *mongoDB) GetBillingLastUpdateTime(owner string, _type common.Type) (bool, time.Time, error) {
filter := bson.M{
"owner": owner,
"type": _type,
Expand Down Expand Up @@ -833,7 +834,7 @@ func (m *mongoDB) QueryBillingRecords(billingRecordQuery *accountv1.BillingRecor
return nil
}

func (m *mongoDB) GetBillingCount(accountType accountv1.Type, startTime, endTime time.Time) (count, amount int64, err error) {
func (m *mongoDB) GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error) {
filter := bson.M{
"type": accountType,
"time": bson.M{
Expand Down
10 changes: 5 additions & 5 deletions controllers/pkg/resources/resources.go
Expand Up @@ -19,11 +19,11 @@ import (
"strings"
"time"

"github.com/labring/sealos/controllers/pkg/common"

"github.com/labring/sealos/controllers/pkg/crypto"
"github.com/labring/sealos/controllers/pkg/utils/logger"

accountv1 "github.com/labring/sealos/controllers/account/api/v1"

"github.com/labring/sealos/controllers/pkg/gpu"
"github.com/labring/sealos/controllers/pkg/utils/env"

Expand Down Expand Up @@ -89,9 +89,9 @@ type Monitor struct {
type BillingType int

type Billing struct {
Time time.Time `json:"time" bson:"time"`
OrderID string `json:"order_id" bson:"order_id"`
Type accountv1.Type `json:"type" bson:"type"`
Time time.Time `json:"time" bson:"time"`
OrderID string `json:"order_id" bson:"order_id"`
Type common.Type `json:"type" bson:"type"`
//Name string `json:"name" bson:"name"`
Namespace string `json:"namespace" bson:"namespace"`
//Used Used `json:"used" bson:"used"`
Expand Down
7 changes: 7 additions & 0 deletions service/account/Dockerfile
@@ -0,0 +1,7 @@
FROM gcr.io/distroless/static:nonroot
ARG TARGETARCH
COPY bin/service-account-$TARGETARCH /manager
EXPOSE 9090
USER 65532:65532

ENTRYPOINT ["/manager"]
55 changes: 55 additions & 0 deletions service/account/Makefile
@@ -0,0 +1,55 @@
IMG ?= ghcr.io/labring/sealos-account-service:latest

# Get the currently used golang install path (in GOPATH/bin, unless GOBIN is set)
ifeq (,$(shell go env GOBIN))
GOBIN=$(shell go env GOPATH)/bin
else
GOBIN=$(shell go env GOBIN)
endif

# only support linux, non cgo
PLATFORMS ?= linux_arm64 linux_amd64
GOOS=linux
CGO_ENABLED=0
GOARCH=$(shell go env GOARCH)

GO_BUILD_FLAGS=-trimpath -ldflags "-s -w"

.PHONY: all
all: build

##@ General

# The help target prints out all targets with their descriptions organized
# beneath their categories. The categories are represented by '##@' and the
# target descriptions by '##'. The awk commands is responsible for reading the
# entire set of makefiles included in this invocation, looking for lines of the
# file as xyz: ## something, and then pretty-format the target and help. Then,
# if there's a line with ##@ something, that gets pretty-printed as a category.
# More info on the usage of ANSI control characters for terminal formatting:
# https://en.wikipedia.org/wiki/ANSI_escape_code#SGR_parameters
# More info on the awk command:
# http://linuxcommand.org/lc3_adv_awk.php

.PHONY: help
help: ## Display this help.
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_0-9-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)

##@ Build

.PHONY: clean
clean:
rm -f $(SERVICE_NAME)

.PHONY: build
build: clean ## Build service-hub binary.
CGO_ENABLED=$(CGO_ENABLED) GOOS=$(GOOS) go build $(GO_BUILD_FLAGS) -o bin/manager main.go

.PHONY: docker-build
docker-build: build
mv bin/manager bin/service-database-${TARGETARCH}
docker build -t $(IMG) .

.PHONY: docker-push
docker-push:
docker push $(IMG)
121 changes: 121 additions & 0 deletions service/account/api/api.go
@@ -0,0 +1,121 @@
package api

import (
"fmt"
"net/http"

"github.com/labring/sealos/service/account/common"

"github.com/labring/sealos/service/account/dao"

"github.com/gin-gonic/gin"
"github.com/labring/sealos/service/account/helper"
)

var _ = helper.NamespaceBillingHistoryReq{}

// @Summary Get namespace billing history list
// @Description Get the billing history namespace list from the database
// @Tags BillingHistory
// @Accept json
// @Produce json
// @Param request body helper.NamespaceBillingHistoryReq true "Namespace billing history request"
// @Success 200 {object} helper.NamespaceBillingHistoryRespData "successfully retrieved namespace billing history list"
// @Failure 400 {object} helper.ErrorMessage "failed to parse namespace billing history request"
// @Failure 401 {object} helper.ErrorMessage "authenticate error"
// @Failure 500 {object} helper.ErrorMessage "failed to get namespace billing history list"
// @Router /account/v1alpha1/namespaces [post]
func GetBillingHistoryNamespaceList(c *gin.Context) {
// Parse the namespace billing history request
req, err := helper.ParseNamespaceBillingHistoryReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, helper.ErrorMessage{Error: fmt.Sprintf("failed to parse namespace billing history request: %v", err)})
return
}
if err := helper.Authenticate(req.Auth); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}

// Get the billing history namespace list from the database
nsList, err := dao.DBClient.GetBillingHistoryNamespaceList(req)
if err != nil {
c.JSON(http.StatusInternalServerError, helper.ErrorMessage{Error: fmt.Sprintf("failed to get namespace billing history list: %v", err)})
return
}
c.JSON(http.StatusOK, helper.NamespaceBillingHistoryResp{
Data: helper.NamespaceBillingHistoryRespData{
List: nsList,
},
Message: "successfully retrieved namespace billing history list",
})
}

// @Summary Get properties
// @Description Get properties from the database
// @Tags Properties
// @Accept json
// @Produce json
// @Param request body helper.Auth true "auth request"
// @Success 200 {object} helper.GetPropertiesResp "successfully retrieved properties"
// @Failure 401 {object} helper.ErrorMessage "authenticate error"
// @Failure 500 {object} helper.ErrorMessage "failed to get properties"
// @Router /account/v1alpha1/properties [post]
func GetProperties(c *gin.Context) {
if err := helper.AuthenticateWithBind(c); err != nil {
c.JSON(http.StatusUnauthorized, helper.ErrorMessage{Error: fmt.Sprintf("authenticate error : %v", err)})
return
}
// Get the properties from the database
properties, err := dao.DBClient.GetProperties()
if err != nil {
c.JSON(http.StatusInternalServerError, fmt.Errorf(fmt.Sprintf("failed to get properties: %v", err)))
return
}
c.JSON(http.StatusOK, helper.GetPropertiesResp{
Data: helper.GetPropertiesRespData{
Properties: properties,
},
Message: "successfully retrieved properties",
})
}

type CostsResult struct {
Data CostsResultData `json:"data" bson:"data"`
Message string `json:"message" bson:"message"`
}

type CostsResultData struct {
Costs common.TimeCostsMap `json:"costs" bson:"costs"`
}

// @Summary Get user costs
// @Description Get user costs within a specified time range
// @Tags Costs
// @Accept json
// @Produce json
// @Param request body helper.UserCostsAmountReq true "User costs amount request"
// @Success 200 {object} map[string]interface{} "successfully retrieved user costs"
// @Failure 400 {object} map[string]interface{} "failed to parse user hour costs amount request"
// @Failure 401 {object} map[string]interface{} "authenticate error"
// @Failure 500 {object} map[string]interface{} "failed to get user costs"
// @Router /account/v1alpha1/costs [post]
func GetCosts(c *gin.Context) {
req, err := helper.ParseUserCostsAmountReq(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to parse user hour costs amount request: %v", err)})
return
}
if err := helper.Authenticate(req.Auth); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": fmt.Sprintf("authenticate error : %v", err)})
return
}
costs, err := dao.DBClient.GetCostAmount(req.Auth.Owner, req.TimeRange.StartTime, req.TimeRange.EndTime)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("failed to get cost : %v", err)})
}
c.JSON(http.StatusOK, CostsResult{
Data: CostsResultData{Costs: costs},
Message: "successfully retrieved user costs",
})
}
10 changes: 10 additions & 0 deletions service/account/common/account.go
@@ -0,0 +1,10 @@
package common

type PropertyQuery struct {
Name string `json:"name,omitempty" bson:"name,omitempty" example:"cpu"`
Alias string `json:"alias,omitempty" bson:"alias,omitempty" example:"gpu-tesla-v100"`
UnitPrice float64 `json:"unit_price,omitempty" bson:"unit_price,omitempty" example:"10000"`
Unit string `json:"unit,omitempty" bson:"unit,omitempty" example:"1m"`
}

type TimeCostsMap [][]interface{}

0 comments on commit 4bea4ea

Please sign in to comment.