diff --git a/.github/workflows/cloud-release.yml b/.github/workflows/cloud-release.yml index a273309ba68..4d8c3454548 100644 --- a/.github/workflows/cloud-release.yml +++ b/.github/workflows/cloud-release.yml @@ -3,6 +3,17 @@ name: Release Cloud on: release: types: [ published ] + workflow_dispatch: + inputs: + tag: + description: 'Tag for manual release' + required: false + default: '' + build_offline_tar_only: + description: 'Build offline tar only' + required: false + default: false + type: boolean env: # Common versions @@ -14,29 +25,33 @@ jobs: uses: ./.github/workflows/import-save-sealos.yml release-controllers: + if: ${{ github.event.inputs.build_offline_tar_only == false }} uses: ./.github/workflows/controllers.yml with: push_image: true - push_image_tag: ${{ github.event.release.tag_name }} + push_image_tag: ${{ github.event.inputs.tag || github.event.release.tag_name }} secrets: inherit release-frontends: + if: ${{ github.event.inputs.build_offline_tar_only == false }} uses: ./.github/workflows/frontend.yml with: push_image: true - push_image_tag: ${{ github.event.release.tag_name }} + push_image_tag: ${{ github.event.inputs.tag || github.event.release.tag_name }} secrets: inherit release-service: + if: ${{ github.event.inputs.build_offline_tar_only == false }} needs: - save-sealos uses: ./.github/workflows/services.yml with: push_image: true - push_image_tag: ${{ github.event.release.tag_name }} + push_image_tag: ${{ github.event.inputs.tag || github.event.release.tag_name }} secrets: inherit release-cloud: + if: ${{ github.event.inputs.build_offline_tar_only == false }} needs: - save-sealos - release-controllers @@ -45,14 +60,17 @@ jobs: uses: ./.github/workflows/cloud.yml with: push_image: true - push_image_tag: ${{ github.event.release.tag_name }} - build_from: ${{ github.event.release.tag_name }} + push_image_tag: ${{ github.event.inputs.tag || github.event.release.tag_name }} + build_from: ${{ github.event.inputs.tag || github.event.release.tag_name }} secrets: inherit release-amd-offline-tar: needs: - release-cloud - runs-on: ubuntu-latest + runs-on: self-hosted + env: + RELEASE_TAG: ${{ github.event.inputs.tag || github.event.release.tag_name }} + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} steps: - name: Checkout uses: actions/checkout@v3 @@ -67,7 +85,11 @@ jobs: sudo mv /tmp/sealos /usr/bin/sealos sudo sealos version - name: Build - run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && export ARCH=amd64 && bash ./scripts/cloud/build-offline-tar.sh + run: | + export CLOUD_VERSION="$RELEASE_TAG" + export VERSION="$RELEASE_TAG" + export ARCH=amd64 + bash ./scripts/cloud/build-offline-tar.sh - name: Setup ossutil uses: manyuanrong/setup-ossutil@v2.0 with: @@ -78,13 +100,16 @@ jobs: run: cat ./sealos-cloud.tar.gz.md5 - name: Upload run: | - ossutil cp ./sealos-cloud.tar.gz oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-amd64.tar.gz - ossutil cp ./sealos-cloud.tar.gz.md5 oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-amd64.tar.gz.md5 + ossutil cp ./sealos-cloud.tar.gz "oss://$OSS_BUCKET/cloud/sealos-cloud-$RELEASE_TAG-amd64.tar.gz" + ossutil cp ./sealos-cloud.tar.gz.md5 "oss://$OSS_BUCKET/cloud/sealos-cloud-$RELEASE_TAG-amd64.tar.gz.md5" release-arm-offline-tar: needs: - release-cloud - runs-on: ubuntu-latest + runs-on: self-hosted + env: + RELEASE_TAG: ${{ github.event.inputs.tag || github.event.release.tag_name }} + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} steps: - name: Checkout uses: actions/checkout@v3 @@ -99,7 +124,11 @@ jobs: sudo mv /tmp/sealos /usr/bin/sealos sudo sealos version - name: Build - run: export CLOUD_VERSION=${{ github.event.release.tag_name }} && export ARCH=arm64 && bash ./scripts/cloud/build-offline-tar.sh + run: | + export CLOUD_VERSION="$RELEASE_TAG" + export VERSION="$RELEASE_TAG" + export ARCH=arm64 + bash ./scripts/cloud/build-offline-tar.sh - name: Setup ossutil uses: manyuanrong/setup-ossutil@v2.0 with: @@ -110,5 +139,5 @@ jobs: run: cat ./sealos-cloud.tar.gz.md5 - name: Upload run: | - ossutil cp ./sealos-cloud.tar.gz oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-arm64.tar.gz - ossutil cp ./sealos-cloud.tar.gz.md5 oss://${{ secrets.OSS_BUCKET }}/cloud/sealos-cloud-${{ github.event.release.tag_name }}-arm64.tar.gz.md5 \ No newline at end of file + ossutil cp ./sealos-cloud.tar.gz "oss://$OSS_BUCKET/cloud/sealos-cloud-$RELEASE_TAG-arm64.tar.gz" + ossutil cp ./sealos-cloud.tar.gz.md5 "oss://$OSS_BUCKET/cloud/sealos-cloud-$RELEASE_TAG-arm64.tar.gz.md5" diff --git a/CHANGELOG/CHANGELOG-5.0.1-beta2.md b/CHANGELOG/CHANGELOG-5.0.1-beta2.md new file mode 100644 index 00000000000..1864a514d6d --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1-beta2.md @@ -0,0 +1,32 @@ +Welcome to the v5.0.1-beta2 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* ebe7f51afe023810fc4e3538f4f2e46007d43002: feat: launchpad support secret userDomains (#5119) (@zjy365) +### Bug fixes +* 2fd2f07892660ce4fa0ad0e167082fbeb372d579: fix: cronjob arm64 image (#5113) (@zijiren233) +* d3ba17bc4925707a32ae999e88019aaf3af23800: fix: node tls reject unauthorized (#5116) (@zijiren233) +* bc0b5f72006a173f9bcac0e287fd9893efe728af: fix: some ui adjust and bug fix (#5097) (@mlhiter) +### Other work +* 8c0c34592369874ce178c956af713dba3c17a9ea: devobx ignore extra ports. (#5112) (@lingdie) +* 1243fd31e8f2f8828e35f1c47e6e6cd19155ae3e: fix desktop and costcenter configs (#5114) (@xudaotutou) +* 22d9a138f9a3f4cd04a4eb927639fd008972adff: fix devbox phase generate. (#5120) (@lingdie) +* b6053c1c2601fafa0752cdaac4501606bd020a20: fix get default property (#5108) (@bxy4543) +* 7b025ae80d215cd1fe860ed08bc69dc132185be3: fix init user (#5104) (@bxy4543) +* d21832075623248f831df5f04b7a8962c3c83fb6: fix scripts: init account jwt secret (#5117) (@bxy4543) +* 3081c5dc1abf65d751de29b52b1febeb3dbbad9a: fix(account-service): fix concurrency issue and add real name info api (#5103) (@HUAHUAI23) +* 7b0be9352fa422f1abca8f232cd8c864db548f7e: release: sealos v5.0.1 beta2 (#5110) (@lingdie) +* 7a4b7b91fe63b3b4747fc99d2753c1b7a409fd39: sealos v5.0.1-beta2 (#5071) (@lingdie) +* 54a3bc28684df2f8427c2ff1cc3969a0dd79c534: style(costcenter): recharge discount (#5111) (@xudaotutou) +* b78691a56704884d1da0544a2779b8f42b45d3f1: sync resourcequota objectstorage/size status used (#5102) (@nowinkeyy) +* c80c265fd6838bbf071cefcfa077a4e4d0274fa4: 🤖 add release changelog using rebot. (#5100) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.1-beta1...v5.0.1-beta2 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG-5.0.1.md b/CHANGELOG/CHANGELOG-5.0.1.md new file mode 100644 index 00000000000..d4d59318e2c --- /dev/null +++ b/CHANGELOG/CHANGELOG-5.0.1.md @@ -0,0 +1,23 @@ +Welcome to the v5.0.1 release of Sealos!🎉🎉! + + + +## Changelog +### New Features +* 2b74a1281cdd72ec5f02a0cc9edf042639a1e054: feat: Implement app guide module (#5115) (@zjy365) +### Bug fixes +* 6489ce6a6d7aaf2aa24ee3514bacab924b0f31a9: fix(desktop):fix merge user (#5101) (@xudaotutou) +### Other work +* 351e7616134950859fe559c57499b6fdff67fa0d: Feat/active task (#5121) (@bxy4543) +* 9817f6fb81b163f42c7d0b31f7e6350101b0f569: devbox cache improve. (#5122) (@lingdie) +* d9b34cd60044590b6a5807d1725398017a735e19: fix build offline scripts and ci. (#5125) (@lingdie) +* a9154f858db2b50ce72e1e0a6fc8b15a954d37a0: fix invaild (#5118) (@xudaotutou) +* 56ffcb829d61b2d5531fe4bb544e17bbe12032ba: 🤖 add release changelog using rebot. (#5123) (@sealos-release-robot) + +**Full Changelog**: https://github.com/labring/sealos/compare/v5.0.1-beta3...v5.0.1 + +See [the CHANGELOG](https://github.com/labring/sealos/blob/main/CHANGELOG/CHANGELOG.md) for more details. + +Your patronage towards Sealos is greatly appreciated 🎉🎉. + +If you encounter any problems during its usage, please create an issue in the [GitHub repository](https://github.com/labring/sealos), we're committed to resolving your problem as soon as possible. diff --git a/CHANGELOG/CHANGELOG.md b/CHANGELOG/CHANGELOG.md index eb182bc5957..cb9cbd44f48 100644 --- a/CHANGELOG/CHANGELOG.md +++ b/CHANGELOG/CHANGELOG.md @@ -2,7 +2,9 @@ All notable changes to this project will be documented in this file. +- [CHANGELOG-5.0.1-beta2.md](./CHANGELOG-5.0.1-beta2.md) - [CHANGELOG-5.0.1-beta1.md](./CHANGELOG-5.0.1-beta1.md) +- [CHANGELOG-5.0.1.md](./CHANGELOG-5.0.1.md) - [CHANGELOG-5.0.0-beta5.md](./CHANGELOG-5.0.0-beta5.md) - [CHANGELOG-5.0.0-beta4.md](./CHANGELOG-5.0.0-beta4.md) - [CHANGELOG-5.0.0-beta3.md](./CHANGELOG-5.0.0-beta3.md) diff --git a/cmd/sealos/cmd/reset.go b/cmd/sealos/cmd/reset.go index c08506878ef..e98c6a83148 100644 --- a/cmd/sealos/cmd/reset.go +++ b/cmd/sealos/cmd/reset.go @@ -31,8 +31,8 @@ reset you current cluster: func newResetCmd() *cobra.Command { resetArgs := &apply.ResetArgs{ - Cluster: &apply.Cluster{}, - SSH: &apply.SSH{}, + ClusterName: &apply.ClusterName{}, + SSH: &apply.SSH{}, } var resetCmd = &cobra.Command{ diff --git a/controllers/account/Makefile b/controllers/account/Makefile index 06035dfcf40..4ae4bcf65af 100644 --- a/controllers/account/Makefile +++ b/controllers/account/Makefile @@ -121,8 +121,8 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions -KUSTOMIZE_VERSION ?= v4.2.0 -CONTROLLER_TOOLS_VERSION ?= v0.8.0 +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize diff --git a/controllers/account/PROJECT b/controllers/account/PROJECT index a045cbb63ae..7ec96ae439b 100644 --- a/controllers/account/PROJECT +++ b/controllers/account/PROJECT @@ -5,13 +5,10 @@ projectName: account repo: github.com/labring/sealos/controllers/account resources: - api: - crdVersion: v1 - namespaced: true controller: true domain: sealos.io group: account kind: Account - path: github.com/labring/sealos/controllers/account/api/v1 version: v1 - api: crdVersion: v1 @@ -39,23 +36,6 @@ resources: group: account kind: Billing version: v1 -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: sealos.io - group: account - kind: BillingRecordQuery - path: github.com/labring/sealos/controllers/account/api/v1 - version: v1 -- api: - crdVersion: v1 - namespaced: true - domain: sealos.io - group: account - kind: PriceQuery - path: github.com/labring/sealos/controllers/account/api/v1 - version: v1 - controller: true group: core kind: Pod @@ -66,31 +46,4 @@ resources: kind: Namespace path: k8s.io/api/core/v1 version: v1 -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: sealos.io - group: account - 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 -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: sealos.io - group: account - kind: BillingInfoQuery - path: github.com/labring/sealos/controllers/account/api/v1 - version: v1 version: "3" diff --git a/controllers/account/api/v1/billinginfoquery_types.go b/controllers/account/api/v1/billinginfoquery_types.go deleted file mode 100644 index 05721066c3e..00000000000 --- a/controllers/account/api/v1/billinginfoquery_types.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -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. - -const ( - QueryTypeNamespacesHistory = "NamespacesHistory" - QueryTypeProperties = "Properties" - QueryTypeAppType = "AppType" - QueryTypeRecharge = "Recharge" -) - -// BillingInfoQuerySpec defines the desired state of BillingInfoQuery -type BillingInfoQuerySpec struct { - QueryType string `json:"queryType"` - Args map[string]string `json:"args,omitempty"` -} - -// BillingInfoQueryStatus defines the observed state of BillingInfoQuery -type BillingInfoQueryStatus struct { - Result string `json:"result"` - Status Status `json:"status"` - StatusDetails string `json:"statusDetails"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// BillingInfoQuery is the Schema for the billinginfoqueries API -type BillingInfoQuery struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec BillingInfoQuerySpec `json:"spec,omitempty"` - Status BillingInfoQueryStatus `json:"status,omitempty"` -} - -type BillingPropertiesForQuery struct { -} - -type PropertyQuery struct { - Name string `json:"name" bson:"name"` - Alias string `json:"alias" bson:"alias"` - UnitPrice float64 `json:"unit_price" bson:"unit_price"` - Unit string `json:"unit" bson:"unit"` -} - -//+kubebuilder:object:root=true - -// BillingInfoQueryList contains a list of BillingInfoQuery -type BillingInfoQueryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []BillingInfoQuery `json:"items"` -} - -func init() { - SchemeBuilder.Register(&BillingInfoQuery{}, &BillingInfoQueryList{}) -} diff --git a/controllers/account/api/v1/billingrecordquery_types.go b/controllers/account/api/v1/billingrecordquery_types.go deleted file mode 100644 index 853894f5cff..00000000000 --- a/controllers/account/api/v1/billingrecordquery_types.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -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" - - "github.com/labring/sealos/controllers/pkg/common" -) - -// 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. - -// BillingRecordQuerySpec defines the desired state of BillingRecordQuery -type BillingRecordQuerySpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - Page int `json:"page"` - PageSize int `json:"pageSize"` - Namespace string `json:"namespace,omitempty"` - StartTime metav1.Time `json:"startTime"` - EndTime metav1.Time `json:"endTime"` - OrderID string `json:"orderID,omitempty"` - Type Type `json:"type"` - AppType string `json:"appType,omitempty"` -} - -// BillingRecordQueryStatus defines the observed state of BillingRecordQuery -type BillingRecordQueryStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file - TotalCount int `json:"totalCount"` - PageLength int `json:"pageLength"` - RechargeAmount int64 `json:"rechargeAmount"` - DeductionAmount int64 `json:"deductionAmount,omitempty"` - Items []BillingRecordQueryItem `json:"item,omitempty"` - Status string `json:"status"` -} - -type BillingRecordQueryItem struct { - Time metav1.Time `json:"time" bson:"time"` - BillingRecordQueryItemInline `json:",inline"` -} - -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 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 - Payment *PaymentForQuery `json:"payment,omitempty" bson:"payment,omitempty"` -} - -type PaymentForQuery struct { - Amount int64 `json:"amount,omitempty" bson:"amount,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// BillingRecordQuery is the Schema for the billingrecordqueries API -type BillingRecordQuery struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec BillingRecordQuerySpec `json:"spec,omitempty"` - Status BillingRecordQueryStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// BillingRecordQueryList contains a list of BillingRecordQuery -type BillingRecordQueryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []BillingRecordQuery `json:"items"` -} - -func init() { - SchemeBuilder.Register(&BillingRecordQuery{}, &BillingRecordQueryList{}) -} diff --git a/controllers/account/api/v1/debt_webhook.go b/controllers/account/api/v1/debt_webhook.go index 942a9f05bd8..cd475ba2935 100644 --- a/controllers/account/api/v1/debt_webhook.go +++ b/controllers/account/api/v1/debt_webhook.go @@ -54,7 +54,7 @@ const ( var logger = logf.Log.WithName("debt-resource") -//+kubebuilder:webhook:path=/validate-v1-sealos-cloud,mutating=false,failurePolicy=ignore,groups="*",resources=*,verbs=create;update;delete,versions=v1,name=debt.sealos.io,admissionReviewVersions=v1,sideEffects=None +//+kubebuilder:webhook:path=/validate-v1-sealos-cloud,mutating=false,failurePolicy=ignore,groups="*",resources=*,verbs=create;update;delete,versions=v1,name=debt.sealos.io,admissionReviewVersions=v1,sideEffects=None,timeoutSeconds=10 // +kubebuilder:object:generate=false type DebtValidate struct { diff --git a/controllers/account/api/v1/namespacebillinghistory_types.go b/controllers/account/api/v1/namespacebillinghistory_types.go deleted file mode 100644 index cb0db06d8f3..00000000000 --- a/controllers/account/api/v1/namespacebillinghistory_types.go +++ /dev/null @@ -1,65 +0,0 @@ -/* -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/pricequery_types.go b/controllers/account/api/v1/pricequery_types.go deleted file mode 100644 index d887903063e..00000000000 --- a/controllers/account/api/v1/pricequery_types.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -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. - -// PriceQuerySpec defines the desired state of PriceQuery -type PriceQuerySpec struct { -} - -// PriceQueryStatus defines the observed state of PriceQuery -type PriceQueryStatus struct { - BillingRecords []BillingRecord `json:"billingRecords,omitempty"` -} - -type BillingRecord struct { - ResourceType string `json:"resourceType"` - Price string `json:"price"` - DiscountType string `json:"discountType,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// PriceQuery is the Schema for the pricequeries API -type PriceQuery struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec PriceQuerySpec `json:"spec,omitempty"` - Status PriceQueryStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// PriceQueryList contains a list of PriceQuery -type PriceQueryList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []PriceQuery `json:"items"` -} - -func init() { - SchemeBuilder.Register(&PriceQuery{}, &PriceQueryList{}) -} diff --git a/controllers/account/api/v1/transfer_types.go b/controllers/account/api/v1/transfer_types.go deleted file mode 100644 index ef4ac535d72..00000000000 --- a/controllers/account/api/v1/transfer_types.go +++ /dev/null @@ -1,88 +0,0 @@ -/* -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 ( - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type TransferState int - -const ( - TransferStatePending TransferState = iota - TransferStateInProgress - TransferStateCompleted - TransferStateFailed -) - -// 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. - -// TransferSpec defines the desired state of Transfer -type TransferSpec struct { - From string `json:"from,omitempty"` - To string `json:"to"` - // +kubebuilder:validation:Minimum=1000000 - Amount int64 `json:"amount"` -} - -// TransferStatus defines the observed state of Transfer -type TransferStatus struct { - Reason string `json:"reason,omitempty"` - Progress TransferState `json:"progress,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// Transfer is the Schema for the transfers API -type Transfer struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec TransferSpec `json:"spec,omitempty"` - Status TransferStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// TransferList contains a list of Transfer -type TransferList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Transfer `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Transfer{}, &TransferList{}) -} - -func (t *Transfer) ToJSON() string { - return `{ - "spec": { - "from": "` + t.Spec.From + `", - "to": "` + t.Spec.To + `", - "amount": ` + fmt.Sprint(t.Spec.Amount) + ` - }, - "status": { - "reason": "` + t.Status.Reason + `", - "progress": "` + string(rune(t.Status.Progress)) + `" - } -}` -} diff --git a/controllers/account/api/v1/zz_generated.deepcopy.go b/controllers/account/api/v1/zz_generated.deepcopy.go index 279d4cc627a..de436afff0d 100644 --- a/controllers/account/api/v1/zz_generated.deepcopy.go +++ b/controllers/account/api/v1/zz_generated.deepcopy.go @@ -1,5 +1,4 @@ //go:build !ignore_autogenerated -// +build !ignore_autogenerated /* Copyright 2023. @@ -131,274 +130,6 @@ func (in *AccountStatus) DeepCopy() *AccountStatus { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingInfoQuery) DeepCopyInto(out *BillingInfoQuery) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingInfoQuery. -func (in *BillingInfoQuery) DeepCopy() *BillingInfoQuery { - if in == nil { - return nil - } - out := new(BillingInfoQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BillingInfoQuery) 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 *BillingInfoQueryList) DeepCopyInto(out *BillingInfoQueryList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]BillingInfoQuery, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingInfoQueryList. -func (in *BillingInfoQueryList) DeepCopy() *BillingInfoQueryList { - if in == nil { - return nil - } - out := new(BillingInfoQueryList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BillingInfoQueryList) 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 *BillingInfoQuerySpec) DeepCopyInto(out *BillingInfoQuerySpec) { - *out = *in - if in.Args != nil { - in, out := &in.Args, &out.Args - *out = make(map[string]string, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingInfoQuerySpec. -func (in *BillingInfoQuerySpec) DeepCopy() *BillingInfoQuerySpec { - if in == nil { - return nil - } - out := new(BillingInfoQuerySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingInfoQueryStatus) DeepCopyInto(out *BillingInfoQueryStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingInfoQueryStatus. -func (in *BillingInfoQueryStatus) DeepCopy() *BillingInfoQueryStatus { - if in == nil { - return nil - } - out := new(BillingInfoQueryStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingPropertiesForQuery) DeepCopyInto(out *BillingPropertiesForQuery) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingPropertiesForQuery. -func (in *BillingPropertiesForQuery) DeepCopy() *BillingPropertiesForQuery { - if in == nil { - return nil - } - out := new(BillingPropertiesForQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingRecord) DeepCopyInto(out *BillingRecord) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecord. -func (in *BillingRecord) DeepCopy() *BillingRecord { - if in == nil { - return nil - } - out := new(BillingRecord) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingRecordQuery) DeepCopyInto(out *BillingRecordQuery) { - *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 BillingRecordQuery. -func (in *BillingRecordQuery) DeepCopy() *BillingRecordQuery { - if in == nil { - return nil - } - out := new(BillingRecordQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BillingRecordQuery) 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 *BillingRecordQueryItem) DeepCopyInto(out *BillingRecordQueryItem) { - *out = *in - in.Time.DeepCopyInto(&out.Time) - in.BillingRecordQueryItemInline.DeepCopyInto(&out.BillingRecordQueryItemInline) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecordQueryItem. -func (in *BillingRecordQueryItem) DeepCopy() *BillingRecordQueryItem { - if in == nil { - return nil - } - out := new(BillingRecordQueryItem) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingRecordQueryItemInline) DeepCopyInto(out *BillingRecordQueryItemInline) { - *out = *in - if in.Costs != nil { - in, out := &in.Costs, &out.Costs - *out = make(Costs, len(*in)) - for key, val := range *in { - (*out)[key] = val - } - } - if in.Payment != nil { - in, out := &in.Payment, &out.Payment - *out = new(PaymentForQuery) - **out = **in - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecordQueryItemInline. -func (in *BillingRecordQueryItemInline) DeepCopy() *BillingRecordQueryItemInline { - if in == nil { - return nil - } - out := new(BillingRecordQueryItemInline) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingRecordQueryList) DeepCopyInto(out *BillingRecordQueryList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]BillingRecordQuery, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecordQueryList. -func (in *BillingRecordQueryList) DeepCopy() *BillingRecordQueryList { - if in == nil { - return nil - } - out := new(BillingRecordQueryList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *BillingRecordQueryList) 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 *BillingRecordQuerySpec) DeepCopyInto(out *BillingRecordQuerySpec) { - *out = *in - in.StartTime.DeepCopyInto(&out.StartTime) - in.EndTime.DeepCopyInto(&out.EndTime) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecordQuerySpec. -func (in *BillingRecordQuerySpec) DeepCopy() *BillingRecordQuerySpec { - if in == nil { - return nil - } - out := new(BillingRecordQuerySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *BillingRecordQueryStatus) DeepCopyInto(out *BillingRecordQueryStatus) { - *out = *in - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]BillingRecordQueryItem, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new BillingRecordQueryStatus. -func (in *BillingRecordQueryStatus) DeepCopy() *BillingRecordQueryStatus { - if in == nil { - return nil - } - out := new(BillingRecordQueryStatus) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Charge) DeepCopyInto(out *Charge) { *out = *in @@ -516,7 +247,9 @@ func (in *DebtStatus) DeepCopyInto(out *DebtStatus) { if in.DebtStatusRecords != nil { in, out := &in.DebtStatusRecords, &out.DebtStatusRecords *out = make([]DebtStatusRecord, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } } @@ -533,6 +266,7 @@ func (in *DebtStatus) DeepCopy() *DebtStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DebtStatusRecord) DeepCopyInto(out *DebtStatusRecord) { *out = *in + in.UpdateTime.DeepCopyInto(&out.UpdateTime) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DebtStatusRecord. @@ -545,102 +279,6 @@ func (in *DebtStatusRecord) DeepCopy() *DebtStatusRecord { 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) - in.Spec.DeepCopyInto(&out.Spec) - in.Status.DeepCopyInto(&out.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 - in.StartTime.DeepCopyInto(&out.StartTime) - in.EndTime.DeepCopyInto(&out.EndTime) -} - -// 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 - if in.NamespaceList != nil { - in, out := &in.NamespaceList, &out.NamespaceList - *out = make([]string, len(*in)) - copy(*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 @@ -668,21 +306,6 @@ func (in *Payment) DeepCopyObject() runtime.Object { return nil } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PaymentForQuery) DeepCopyInto(out *PaymentForQuery) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PaymentForQuery. -func (in *PaymentForQuery) DeepCopy() *PaymentForQuery { - if in == nil { - return nil - } - out := new(PaymentForQuery) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *PaymentList) DeepCopyInto(out *PaymentList) { *out = *in @@ -744,201 +367,3 @@ func (in *PaymentStatus) DeepCopy() *PaymentStatus { in.DeepCopyInto(out) return out } - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PriceQuery) DeepCopyInto(out *PriceQuery) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec - in.Status.DeepCopyInto(&out.Status) -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriceQuery. -func (in *PriceQuery) DeepCopy() *PriceQuery { - if in == nil { - return nil - } - out := new(PriceQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PriceQuery) 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 *PriceQueryList) DeepCopyInto(out *PriceQueryList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]PriceQuery, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriceQueryList. -func (in *PriceQueryList) DeepCopy() *PriceQueryList { - if in == nil { - return nil - } - out := new(PriceQueryList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *PriceQueryList) 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 *PriceQuerySpec) DeepCopyInto(out *PriceQuerySpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriceQuerySpec. -func (in *PriceQuerySpec) DeepCopy() *PriceQuerySpec { - if in == nil { - return nil - } - out := new(PriceQuerySpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PriceQueryStatus) DeepCopyInto(out *PriceQueryStatus) { - *out = *in - if in.BillingRecords != nil { - in, out := &in.BillingRecords, &out.BillingRecords - *out = make([]BillingRecord, len(*in)) - copy(*out, *in) - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PriceQueryStatus. -func (in *PriceQueryStatus) DeepCopy() *PriceQueryStatus { - if in == nil { - return nil - } - out := new(PriceQueryStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PropertyQuery) DeepCopyInto(out *PropertyQuery) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PropertyQuery. -func (in *PropertyQuery) DeepCopy() *PropertyQuery { - if in == nil { - return nil - } - out := new(PropertyQuery) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Transfer) DeepCopyInto(out *Transfer) { - *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 Transfer. -func (in *Transfer) DeepCopy() *Transfer { - if in == nil { - return nil - } - out := new(Transfer) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Transfer) 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 *TransferList) DeepCopyInto(out *TransferList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Transfer, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TransferList. -func (in *TransferList) DeepCopy() *TransferList { - if in == nil { - return nil - } - out := new(TransferList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *TransferList) 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 *TransferSpec) DeepCopyInto(out *TransferSpec) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TransferSpec. -func (in *TransferSpec) DeepCopy() *TransferSpec { - if in == nil { - return nil - } - out := new(TransferSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TransferStatus) DeepCopyInto(out *TransferStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TransferStatus. -func (in *TransferStatus) DeepCopy() *TransferStatus { - if in == nil { - return nil - } - out := new(TransferStatus) - in.DeepCopyInto(out) - return out -} diff --git a/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml index ab6de6f2fb5..f47c776de17 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_accounts.yaml @@ -1,4 +1,4 @@ -# Copyright © 2023 sealos. +# Copyright © 2024 sealos. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: accounts.account.sealos.io spec: group: account.sealos.io @@ -35,14 +34,19 @@ spec: description: Account is the Schema for the accounts 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' + 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' + 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 @@ -101,9 +105,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_billinginfoqueries.yaml b/controllers/account/config/crd/bases/account.sealos.io_billinginfoqueries.yaml deleted file mode 100644 index e5fcd756538..00000000000 --- a/controllers/account/config/crd/bases/account.sealos.io_billinginfoqueries.yaml +++ /dev/null @@ -1,85 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: billinginfoqueries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: BillingInfoQuery - listKind: BillingInfoQueryList - plural: billinginfoqueries - singular: billinginfoquery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: BillingInfoQuery is the Schema for the billinginfoqueries 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: BillingInfoQuerySpec defines the desired state of BillingInfoQuery - properties: - args: - additionalProperties: - type: string - type: object - queryType: - type: string - required: - - queryType - type: object - status: - description: BillingInfoQueryStatus defines the observed state of BillingInfoQuery - properties: - result: - type: string - status: - type: string - statusDetails: - type: string - required: - - result - - status - - statusDetails - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_billingrecordqueries.yaml b/controllers/account/config/crd/bases/account.sealos.io_billingrecordqueries.yaml deleted file mode 100644 index 798603fc156..00000000000 --- a/controllers/account/config/crd/bases/account.sealos.io_billingrecordqueries.yaml +++ /dev/null @@ -1,153 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: billingrecordqueries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: BillingRecordQuery - listKind: BillingRecordQueryList - plural: billingrecordqueries - singular: billingrecordquery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: BillingRecordQuery is the Schema for the billingrecordqueries - 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: BillingRecordQuerySpec defines the desired state of BillingRecordQuery - properties: - appType: - type: string - endTime: - format: date-time - type: string - namespace: - type: string - orderID: - type: string - page: - description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - Important: Run "make" to regenerate code after modifying this file' - type: integer - pageSize: - type: integer - startTime: - format: date-time - type: string - type: - type: integer - required: - - endTime - - page - - pageSize - - startTime - - type - type: object - status: - description: BillingRecordQueryStatus defines the observed state of BillingRecordQuery - properties: - deductionAmount: - format: int64 - type: integer - item: - items: - properties: - amount: - description: Amount = PaymentAmount + GiftAmount - format: int64 - type: integer - appType: - type: string - costs: - additionalProperties: - format: int64 - type: integer - type: object - name: - type: string - namespace: - type: string - order_id: - type: string - payment: - description: when Type = Recharge, PaymentAmount is the amount - of recharge - properties: - amount: - format: int64 - type: integer - type: object - time: - format: date-time - type: string - type: - type: integer - required: - - order_id - - time - - type - type: object - type: array - pageLength: - type: integer - rechargeAmount: - format: int64 - type: integer - status: - type: string - totalCount: - description: 'INSERT ADDITIONAL STATUS FIELD - define observed state - of cluster Important: Run "make" to regenerate code after modifying - this file' - type: integer - required: - - pageLength - - rechargeAmount - - status - - totalCount - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_debts.yaml b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml index ae0db6f8040..262cfc5fd7e 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_debts.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_debts.yaml @@ -1,4 +1,4 @@ -# Copyright © 2023 sealos. +# Copyright © 2024 sealos. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: debts.account.sealos.io spec: group: account.sealos.io @@ -39,24 +38,29 @@ spec: description: Debt is the Schema for the debts 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' + 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' + 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: DebtSpec defines the desired state of Debt properties: - userName: - type: string userID: type: string + userName: + type: string type: object status: description: DebtStatus defines the observed state of Debt @@ -85,9 +89,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml b/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml deleted file mode 100644 index 33d310f9169..00000000000 --- a/controllers/account/config/crd/bases/account.sealos.io_namespacebillinghistories.yaml +++ /dev/null @@ -1,91 +0,0 @@ -# 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. - ---- -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/bases/account.sealos.io_payments.yaml b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml index d5e0ad03a3a..0d62f42da27 100644 --- a/controllers/account/config/crd/bases/account.sealos.io_payments.yaml +++ b/controllers/account/config/crd/bases/account.sealos.io_payments.yaml @@ -1,4 +1,4 @@ -# Copyright © 2023 sealos. +# Copyright © 2024 sealos. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: payments.account.sealos.io spec: group: account.sealos.io @@ -35,14 +34,19 @@ spec: description: Payment is the Schema for the payments 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' + 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' + 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 @@ -57,12 +61,12 @@ spec: default: wechat description: e.g. wechat, alipay, creditcard, etc. type: string - userID: - description: UserID is the user id who want to recharge - type: string userCR: description: UserCr is the user cr name who want to recharge type: string + userID: + description: UserID is the user id who want to recharge + type: string type: object status: description: PaymentStatus defines the observed state of Payment @@ -83,9 +87,3 @@ spec: storage: true subresources: status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_pricequeries.yaml b/controllers/account/config/crd/bases/account.sealos.io_pricequeries.yaml deleted file mode 100644 index b73fddd52f5..00000000000 --- a/controllers/account/config/crd/bases/account.sealos.io_pricequeries.yaml +++ /dev/null @@ -1,80 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: pricequeries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: PriceQuery - listKind: PriceQueryList - plural: pricequeries - singular: pricequery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: PriceQuery is the Schema for the pricequeries 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: PriceQuerySpec defines the desired state of PriceQuery - type: object - status: - description: PriceQueryStatus defines the observed state of PriceQuery - properties: - billingRecords: - items: - properties: - discountType: - type: string - price: - type: string - resourceType: - type: string - required: - - price - - resourceType - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] diff --git a/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml b/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml deleted file mode 100644 index f746906d384..00000000000 --- a/controllers/account/config/crd/bases/account.sealos.io_transfers.yaml +++ /dev/null @@ -1,82 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: transfers.account.sealos.io -spec: - group: account.sealos.io - names: - kind: Transfer - listKind: TransferList - plural: transfers - singular: transfer - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Transfer is the Schema for the transfers 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: TransferSpec defines the desired state of Transfer - properties: - amount: - format: int64 - minimum: 1000000 - type: integer - from: - type: string - to: - type: string - required: - - amount - - to - type: object - status: - description: TransferStatus defines the observed state of Transfer - properties: - progress: - type: integer - reason: - 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 2fecd447f98..c3ece071eb5 100644 --- a/controllers/account/config/crd/kustomization.yaml +++ b/controllers/account/config/crd/kustomization.yaml @@ -16,14 +16,8 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: -- bases/account.sealos.io_accounts.yaml - bases/account.sealos.io_payments.yaml - bases/account.sealos.io_debts.yaml -- bases/account.sealos.io_billingrecordqueries.yaml -- bases/account.sealos.io_pricequeries.yaml -- bases/account.sealos.io_transfers.yaml -- bases/account.sealos.io_namespacebillinghistories.yaml -- bases/account.sealos.io_billinginfoqueries.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/controllers/account/config/crd/patches/cainjection_in_billinginfoqueries.yaml b/controllers/account/config/crd/patches/cainjection_in_billinginfoqueries.yaml deleted file mode 100644 index bf5da442df1..00000000000 --- a/controllers/account/config/crd/patches/cainjection_in_billinginfoqueries.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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: billinginfoqueries.account.sealos.io diff --git a/controllers/account/config/crd/patches/cainjection_in_billingrecordqueries.yaml b/controllers/account/config/crd/patches/cainjection_in_billingrecordqueries.yaml deleted file mode 100644 index 086f6fb6228..00000000000 --- a/controllers/account/config/crd/patches/cainjection_in_billingrecordqueries.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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: billingrecordqueries.account.sealos.io diff --git a/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml b/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml deleted file mode 100644 index e7849ccc4d8..00000000000 --- a/controllers/account/config/crd/patches/cainjection_in_namespacebillinghistories.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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/cainjection_in_pricequeries.yaml b/controllers/account/config/crd/patches/cainjection_in_pricequeries.yaml deleted file mode 100644 index 58fb3e5702c..00000000000 --- a/controllers/account/config/crd/patches/cainjection_in_pricequeries.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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: pricequeries.account.sealos.io diff --git a/controllers/account/config/crd/patches/webhook_in_billinginfoqueries.yaml b/controllers/account/config/crd/patches/webhook_in_billinginfoqueries.yaml deleted file mode 100644 index fd3f8f8fcd0..00000000000 --- a/controllers/account/config/crd/patches/webhook_in_billinginfoqueries.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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. - -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: billinginfoqueries.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/crd/patches/webhook_in_billingrecordqueries.yaml b/controllers/account/config/crd/patches/webhook_in_billingrecordqueries.yaml deleted file mode 100644 index 54615e7aac6..00000000000 --- a/controllers/account/config/crd/patches/webhook_in_billingrecordqueries.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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. - -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: billingrecordqueries.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/crd/patches/webhook_in_namespacebillinghistories.yaml b/controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml deleted file mode 100644 index 3361858e04c..00000000000 --- a/controllers/account/config/crd/patches/webhook_in_namespacebillinghistories.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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. - -# 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/crd/patches/webhook_in_pricequeries.yaml b/controllers/account/config/crd/patches/webhook_in_pricequeries.yaml deleted file mode 100644 index 5740f4c8963..00000000000 --- a/controllers/account/config/crd/patches/webhook_in_pricequeries.yaml +++ /dev/null @@ -1,30 +0,0 @@ -# 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. - -# The following patch enables a conversion webhook for the CRD -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - name: pricequeries.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/default/manager_auth_proxy_patch.yaml b/controllers/account/config/default/manager_auth_proxy_patch.yaml index a3a0d8d5b0b..405c9093a9b 100644 --- a/controllers/account/config/default/manager_auth_proxy_patch.yaml +++ b/controllers/account/config/default/manager_auth_proxy_patch.yaml @@ -50,49 +50,38 @@ spec: - secretRef: name: payment-secret optional: true + - configMapRef: + name: account-manager-env securityContext: runAsNonRoot: true allowPrivilegeEscalation: false env: - - name: DOMAIN - value: '{{ .cloudDomain }}' - - name: PORT - value: '{{ .cloudPort }}' - name: ACCOUNT_NAMESPACE - value: "sealos-system" + value: sealos-system - name: NAMESPACE_NAME - value: "user-system" - - name: NEW_ACCOUNT_AMOUNT - value: "ri79LzQiQrs6CVa1ctE308+AseBXbOua0RIMCXAH5hc3irs=" + value: user-system - name: WHITELIST - value: "notifications.Notification.notification.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,billinginfoqueries.BillingInfoQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1" + value: licenses.License.license.sealos.io/v1,notifications.Notification.notification.sealos.io/v1,payments.Payment.account.sealos.io/v1,billingrecordqueries.BillingRecordQuery.account.sealos.io/v1,billinginfoqueries.BillingInfoQuery.account.sealos.io/v1,pricequeries.PriceQuery.account.sealos.io/v1 - name: ACCOUNT_SYSTEM_NAMESPACE valueFrom: fieldRef: apiVersion: v1 fieldPath: metadata.namespace - - name: MONGO_URI - valueFrom: - secretKeyRef: - name: mongo-secret - key: MONGO_URI - - name: ApproachingDeletionPeriod - value: "345600" - - name: ImminentDeletionPeriod - value: "259200" - - name: FinalDeletionPeriod - value: "604800" - - name: DebtDetectionCycleSeconds - value: "30" - - name: OSAdminSecret - value: '{{ .OSAdminSecret }}' - - name: OSInternalEndpoint - value: '{{ .OSInternalEndpoint }}' - - name: oSNamespace - value: '{{ .OSNamespace }}' image: ghcr.io/labring/sealos-account-controller:latest imagePullPolicy: Always args: - "--health-probe-bind-address=:8081" - "--metrics-bind-address=127.0.0.1:8080" - "--leader-elect" + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: "control-plane" + operator: In + values: + - controller-manager + topologyKey: "kubernetes.io/hostname" \ No newline at end of file diff --git a/controllers/account/config/rbac/billinginfoquery_editor_role.yaml b/controllers/account/config/rbac/billinginfoquery_editor_role.yaml deleted file mode 100644 index 3e53d63f1ed..00000000000 --- a/controllers/account/config/rbac/billinginfoquery_editor_role.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# permissions for end users to edit billinginfoqueries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: billinginfoquery-editor-role -rules: -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/status - verbs: - - get diff --git a/controllers/account/config/rbac/billinginfoquery_viewer_role.yaml b/controllers/account/config/rbac/billinginfoquery_viewer_role.yaml deleted file mode 100644 index 0c779bd3971..00000000000 --- a/controllers/account/config/rbac/billinginfoquery_viewer_role.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -# permissions for end users to view billinginfoqueries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: billinginfoquery-viewer-role -rules: -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries - verbs: - - get - - list - - watch -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/status - verbs: - - get diff --git a/controllers/account/config/rbac/billingrecordquery_editor_role.yaml b/controllers/account/config/rbac/billingrecordquery_editor_role.yaml deleted file mode 100644 index c4fca751b42..00000000000 --- a/controllers/account/config/rbac/billingrecordquery_editor_role.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# permissions for end users to edit billingrecordqueries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: billingrecordquery-editor-role -rules: -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/status - verbs: - - get diff --git a/controllers/account/config/rbac/billingrecordquery_viewer_role.yaml b/controllers/account/config/rbac/billingrecordquery_viewer_role.yaml deleted file mode 100644 index e9edac3624f..00000000000 --- a/controllers/account/config/rbac/billingrecordquery_viewer_role.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -# permissions for end users to view billingrecordqueries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: billingrecordquery-viewer-role -rules: -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries - verbs: - - get - - list - - watch -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/status - verbs: - - get diff --git a/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml b/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml deleted file mode 100644 index 4a3625eff72..00000000000 --- a/controllers/account/config/rbac/namespacebillinghistory_editor_role.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# 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 deleted file mode 100644 index 4bb420e4cc1..00000000000 --- a/controllers/account/config/rbac/namespacebillinghistory_viewer_role.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -# 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/pricequery_editor_role.yaml b/controllers/account/config/rbac/pricequery_editor_role.yaml deleted file mode 100644 index 56c1e4dfa04..00000000000 --- a/controllers/account/config/rbac/pricequery_editor_role.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# permissions for end users to edit pricequeries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: pricequery-editor-role -rules: -- apiGroups: - - account.sealos.io - resources: - - pricequeries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - pricequeries/status - verbs: - - get diff --git a/controllers/account/config/rbac/pricequery_viewer_role.yaml b/controllers/account/config/rbac/pricequery_viewer_role.yaml deleted file mode 100644 index 218a7dfeb77..00000000000 --- a/controllers/account/config/rbac/pricequery_viewer_role.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -# permissions for end users to view pricequeries. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: pricequery-viewer-role -rules: -- apiGroups: - - account.sealos.io - resources: - - pricequeries - verbs: - - get - - list - - watch -- apiGroups: - - account.sealos.io - resources: - - pricequeries/status - verbs: - - get diff --git a/controllers/account/config/rbac/role.yaml b/controllers/account/config/rbac/role.yaml index 5d6826128dd..ae3b9773c50 100644 --- a/controllers/account/config/rbac/role.yaml +++ b/controllers/account/config/rbac/role.yaml @@ -1,4 +1,4 @@ -# Copyright © 2023 sealos. +# Copyright © 2024 sealos. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: @@ -31,12 +30,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - configmaps/status - verbs: - - get - apiGroups: - "" resources: @@ -75,58 +68,6 @@ rules: - get - patch - update -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/status - verbs: - - get - - patch - - update -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/status - verbs: - - get - - patch - - update - apiGroups: - account.sealos.io resources: @@ -153,32 +94,6 @@ 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: @@ -205,32 +120,6 @@ rules: - get - patch - update -- apiGroups: - - account.sealos.io - resources: - - pricequeries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - pricequeries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - pricequeries/status - verbs: - - get - - patch - - update - apiGroups: - apps resources: @@ -448,6 +337,7 @@ rules: resources: - users verbs: + - create - get - list - watch diff --git a/controllers/account/config/rbac/transfer_editor_role.yaml b/controllers/account/config/rbac/transfer_editor_role.yaml deleted file mode 100644 index 6c8bfffef57..00000000000 --- a/controllers/account/config/rbac/transfer_editor_role.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# permissions for end users to edit transfers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: transfer-editor-role -rules: -- apiGroups: - - account.sealos.io - resources: - - transfers - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - transfers/status - verbs: - - get diff --git a/controllers/account/config/rbac/transfer_viewer_role.yaml b/controllers/account/config/rbac/transfer_viewer_role.yaml deleted file mode 100644 index 253667def01..00000000000 --- a/controllers/account/config/rbac/transfer_viewer_role.yaml +++ /dev/null @@ -1,34 +0,0 @@ -# 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. - -# permissions for end users to view transfers. -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: transfer-viewer-role -rules: -- apiGroups: - - account.sealos.io - resources: - - transfers - verbs: - - get - - list - - watch -- apiGroups: - - account.sealos.io - resources: - - transfers/status - verbs: - - get diff --git a/controllers/account/config/samples/account_v1_billinginfoquery.yaml b/controllers/account/config/samples/account_v1_billinginfoquery.yaml deleted file mode 100644 index 9a728da77b6..00000000000 --- a/controllers/account/config/samples/account_v1_billinginfoquery.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -apiVersion: account.sealos.io/v1 -kind: BillingInfoQuery -metadata: - name: billinginfoquery-sample -spec: - # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_billingrecordquery.yaml b/controllers/account/config/samples/account_v1_billingrecordquery.yaml deleted file mode 100644 index 524b569511a..00000000000 --- a/controllers/account/config/samples/account_v1_billingrecordquery.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -apiVersion: account.sealos.io/v1 -kind: BillingRecordQuery -metadata: - name: billingrecordquery-sample -spec: - # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml b/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml deleted file mode 100644 index de10eb0c693..00000000000 --- a/controllers/account/config/samples/account_v1_namespacebillinghistory.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -apiVersion: account.sealos.io/v1 -kind: NamespaceBillingHistory -metadata: - name: namespacebillinghistory-sample -spec: - # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_pricequery.yaml b/controllers/account/config/samples/account_v1_pricequery.yaml deleted file mode 100644 index 23da8793581..00000000000 --- a/controllers/account/config/samples/account_v1_pricequery.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -apiVersion: account.sealos.io/v1 -kind: PriceQuery -metadata: - name: pricequery-sample -spec: - # TODO(user): Add fields here diff --git a/controllers/account/config/samples/account_v1_transfer.yaml b/controllers/account/config/samples/account_v1_transfer.yaml deleted file mode 100644 index dc584ca24cc..00000000000 --- a/controllers/account/config/samples/account_v1_transfer.yaml +++ /dev/null @@ -1,20 +0,0 @@ -# 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. - -apiVersion: account.sealos.io/v1 -kind: Transfer -metadata: - name: transfer-sample -spec: - # TODO(user): Add fields here diff --git a/controllers/account/config/webhook/manifests.yaml b/controllers/account/config/webhook/manifests.yaml index fe4fb619f2c..d48ea280383 100644 --- a/controllers/account/config/webhook/manifests.yaml +++ b/controllers/account/config/webhook/manifests.yaml @@ -1,4 +1,4 @@ -# Copyright © 2023 sealos. +# Copyright © 2024 sealos. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,27 +16,40 @@ apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: - creationTimestamp: null name: validating-webhook-configuration webhooks: - admissionReviewVersions: - - v1 + - v1 clientConfig: service: - name: webhook-service - namespace: system + name: account-webhook-service + namespace: account-system path: /validate-v1-sealos-cloud + timeoutSeconds: 10 failurePolicy: Ignore name: debt.sealos.io + namespaceSelector: + matchExpressions: + - key: user.sealos.io/owner + operator: Exists rules: - - apiGroups: - - '*' - apiVersions: - - v1 - operations: - - CREATE - - UPDATE - - DELETE - resources: - - '*' + - apiGroups: + - '*' + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + - DELETE + resources: + - '*' + - apiGroups: + - account.sealos.io + apiVersions: + - v1 + operations: + - UPDATE + resources: + - payments/status + scope: '*' sideEffects: None diff --git a/controllers/account/controllers/account_controller.go b/controllers/account/controllers/account_controller.go index b20dbbaf664..7024d150cfb 100644 --- a/controllers/account/controllers/account_controller.go +++ b/controllers/account/controllers/account_controller.go @@ -23,6 +23,7 @@ import ( "fmt" "math" "os" + "sort" "strconv" "strings" "time" @@ -60,14 +61,10 @@ import ( ) const ( - ACCOUNTNAMESPACEENV = "ACCOUNT_NAMESPACE" - DEFAULTACCOUNTNAMESPACE = "sealos-system" - AccountAnnotationNewAccount = "account.sealos.io/new-account" - AccountAnnotationIgnoreQuota = "account.sealos.io/ignore-quota" - NEWACCOUNTAMOUNTENV = "NEW_ACCOUNT_AMOUNT" - RECHARGEGIFT = "recharge-gift" - SEALOS = "sealos" - DefaultInitialBalance = 5_000_000 + ACCOUNTNAMESPACEENV = "ACCOUNT_NAMESPACE" + DEFAULTACCOUNTNAMESPACE = "sealos-system" + RECHARGEGIFT = "recharge-gift" + SEALOS = "sealos" ) // AccountReconciler reconciles an Account object @@ -89,7 +86,7 @@ type AccountReconciler struct { //+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/finalizers,verbs=update //+kubebuilder:rbac:groups=core,resources=resourcequotas,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=limitranges,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=user.sealos.io,resources=users,verbs=get;list;watch +//+kubebuilder:rbac:groups=user.sealos.io,resources=users,verbs=create;get;list;watch //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;patch;delete @@ -232,58 +229,14 @@ func parseConfigList(s string, list interface{}, configName string) error { return nil } -//func GetUserOwner(user *userv1.User) string { -// own := user.Annotations[userv1.UserAnnotationOwnerKey] -// if own == "" { -// return user.Name -// } -// return own -//} - const BaseUnit = 1_000_000 -//func (r *AccountReconciler) getAmountWithRates(amount int64, account *pkgtypes.Account) (amt int64, err error) { -// //userActivities, err := pkgtypes.ParseUserActivities(account.Annotations) -// //if err != nil { -// // return nil, 0, fmt.Errorf("parse user activities failed: %w", err) -// //} -// // -// //rechargeDiscount := pkgtypes.RechargeDiscount{ -// // DiscountSteps: r.RechargeStep, -// // DiscountRates: r.RechargeRatio, -// //} -// //if len(userActivities) > 0 { -// // if activityType, phase, _ := pkgtypes.GetUserActivityDiscount(r.Activities, &userActivities); phase != nil { -// // if len(phase.RechargeDiscount.DiscountSteps) > 0 { -// // rechargeDiscount.DiscountSteps = phase.RechargeDiscount.DiscountSteps -// // rechargeDiscount.DiscountRates = phase.RechargeDiscount.DiscountRates -// // } -// // rechargeDiscount.SpecialDiscount = phase.RechargeDiscount.SpecialDiscount -// // rechargeDiscount = phase.RechargeDiscount -// // currentPhase := userActivities[activityType].Phases[userActivities[activityType].CurrentPhase] -// // anno = pkgtypes.SetUserPhaseRechargeTimes(account.Annotations, activityType, currentPhase.Name, currentPhase.RechargeNums+1) -// // } -// //} -// //return anno, getAmountWithDiscount(amount, rechargeDiscount), nil -// -// discount, err := r.AccountV2.GetUserAccountRechargeDiscount(&pkgtypes.UserQueryOpts{UID: account.UserUID}) -// if err != nil { -// return 0, fmt.Errorf("get user %s account recharge discount failed: %w", account.UserUID, err) -// } -// if discount == nil || discount.DiscountSteps == nil || discount.DiscountRates == nil { -// return getAmountWithDiscount(amount, r.DefaultDiscount), nil -// } -// return getAmountWithDiscount(amount, *discount), nil -//} - -func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int64 { - if discount.SpecialDiscount != nil && discount.SpecialDiscount[amount/BaseUnit] != 0 { - return amount + discount.SpecialDiscount[amount/BaseUnit]*BaseUnit - } +func getAmountWithDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) int64 { var r float64 - for i, s := range discount.DiscountSteps { - if amount >= s*BaseUnit { - r = discount.DiscountRates[i] + for _, step := range sortSteps(discount.DefaultSteps) { + ratio := discount.DefaultSteps[step] + if amount >= step*BaseUnit { + r = ratio } else { break } @@ -291,6 +244,23 @@ func getAmountWithDiscount(amount int64, discount pkgtypes.RechargeDiscount) int return int64(math.Ceil(float64(amount) * r / 100)) } +func sortSteps(steps map[int64]float64) (keys []int64) { + for k := range steps { + keys = append(keys, k) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + return +} + +func getFirstRechargeDiscount(amount int64, discount pkgtypes.UserRechargeDiscount) (bool, int64) { + if discount.FirstRechargeSteps != nil && discount.FirstRechargeSteps[amount/BaseUnit] != 0 { + return true, int64(math.Ceil(float64(amount) * discount.FirstRechargeSteps[amount/BaseUnit] / 100)) + } + return false, getAmountWithDiscount(amount, discount) +} + func (r *AccountReconciler) BillingCVM() error { cvmMap, err := r.CVMDBClient.GetPendingStateInstance(os.Getenv("LOCAL_REGION")) if err != nil { diff --git a/controllers/account/controllers/account_controller_test.go b/controllers/account/controllers/account_controller_test.go index d23bd5bfb29..dece29aaf2e 100644 --- a/controllers/account/controllers/account_controller_test.go +++ b/controllers/account/controllers/account_controller_test.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "encoding/json" "os" "testing" @@ -26,71 +27,8 @@ import ( "github.com/labring/sealos/controllers/pkg/database" "github.com/labring/sealos/controllers/pkg/database/cockroach" "github.com/labring/sealos/controllers/pkg/database/mongo" - - "github.com/labring/sealos/controllers/pkg/types" ) -//func Test_giveGift(t *testing.T) { -// type args struct { -// amount int64 -// } -// const BaseUnit = 1_000_000 -// tests := []struct { -// name string -// args args -// want int64 -// }{ -// // [1-298] -> 0%, [299-598] -> 10%, [599-1998] -> 15%, [1999-4998] -> 20%, [4999-19998] -> 25%, [19999+] -> 30% -// {name: "0% less than 299", args: args{amount: 100 * BaseUnit}, want: 0 + 100*BaseUnit}, -// {name: "10% between 299 and 599", args: args{amount: 299 * BaseUnit}, want: 29_900_000 + 299*BaseUnit}, -// {name: "10% between 299 and 599", args: args{amount: 300 * BaseUnit}, want: 30_000_000 + 300*BaseUnit}, -// {name: "15% between 599 and 1999", args: args{amount: 599 * BaseUnit}, want: 89_850_000 + 599*BaseUnit}, -// {name: "15% between 599 and 1999", args: args{amount: 600 * BaseUnit}, want: 90_000_000 + 600*BaseUnit}, -// {name: "20% between 1999 and 4999", args: args{amount: 1999 * BaseUnit}, want: 399_800_000 + 1999*BaseUnit}, -// {name: "20% between 1999 and 4999", args: args{amount: 2000 * BaseUnit}, want: 400_000_000 + 2000*BaseUnit}, -// {name: "25% between 4999 and 19999", args: args{amount: 4999 * BaseUnit}, want: 1249_750_000 + 4999*BaseUnit}, -// {name: "25% between 4999 and 19999", args: args{amount: 5000 * BaseUnit}, want: 1250_000_000 + 5000*BaseUnit}, -// {name: "30% more than 19999", args: args{amount: 19999 * BaseUnit}, want: 5999_700_000 + 19999*BaseUnit}, -// {name: "30% more than 19999", args: args{amount: 20000 * BaseUnit}, want: 6000_000_000 + 20000*BaseUnit}, -// {name: "30% more than 19999", args: args{amount: 99999 * BaseUnit}, want: 29999_700_000 + 99999*BaseUnit}, -// {"0% less than 299", args{amount: 1 * BaseUnit}, 1 * BaseUnit}, -// } -// -// configMap := &corev1.ConfigMap{} -// configMap.Data = make(map[string]string) -// configMap.Data["steps"] = "299,599,1999,4999,19999" -// configMap.Data["ratios"] = "10.0,15.0,20.0,25.0,30.0" -// -// for _, tt := range tests { -// t.Run(tt.name, func(t *testing.T) { -// if got, _ := giveGift(tt.args.amount, configMap); got != tt.want { -// t.Errorf("giveGift() = %v, want %v", got, tt.want) -// } -// }) -// } -//} - -func Test_getAmountWithDiscount(t *testing.T) { - type args struct { - amount int64 - discount types.RechargeDiscount - } - tests := []struct { - name string - args args - want int64 - }{ - // TODO: Add test cases. - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if got := getAmountWithDiscount(tt.args.amount, tt.args.discount); got != tt.want { - t.Errorf("getAmountWithDiscount() = %v, want %v", got, tt.want) - } - }) - } -} - func TestAccountReconciler_BillingCVM(t *testing.T) { dbCtx := context.Background() cvmDBClient, err := mongo.NewMongoInterface(dbCtx, os.Getenv(database.CVMMongoURI)) @@ -136,3 +74,45 @@ func TestAccountReconciler_BillingCVM(t *testing.T) { t.Errorf("AccountReconciler.BillingCVM() error = %v", err) } } + +func TestAccountV2_GetAccountConfig(t *testing.T) { + os.Setenv("LOCAL_REGION", "") + v2Account, err := cockroach.NewCockRoach("", "") + if err != nil { + t.Fatalf("unable to connect to cockroach: %v", err) + } + defer func() { + err := v2Account.Close() + if err != nil { + t.Errorf("unable to disconnect from cockroach: %v", err) + } + }() + err = v2Account.InitTables() + if err != nil { + t.Fatalf("unable to init tables: %v", err) + } + + //if err = v2Account.InsertAccountConfig(&types.AccountConfig{ + // TaskProcessRegion: "192.160.0.55.nip.io", + // FirstRechargeDiscountSteps: map[int64]float64{ + // 8: 100, 32: 100, 128: 100, 256: 100, 512: 100, 1024: 100, + // }, + // DefaultDiscountSteps: map[int64]float64{ + // //128,256,512,1024,2048,4096; 10,15,20,25,30,35 + // 128: 10, 256: 15, 512: 20, 1024: 25, 2048: 30, 4096: 35, + // }, + //}); err != nil { + // t.Fatalf("unable to insert account config: %v", err) + //} + + aa, err := v2Account.GetAccountConfig() + if err != nil { + t.Fatalf("failed to get account config: %v", err) + } + + data, err := json.MarshalIndent(aa, "", " ") + if err != nil { + t.Fatalf("failed to marshal account config: %v", err) + } + t.Logf("success get account config:\n%s", string(data)) +} diff --git a/controllers/account/controllers/accountv2_test.go b/controllers/account/controllers/accountv2_test.go index 62fd16ce21d..34f37ce058d 100644 --- a/controllers/account/controllers/accountv2_test.go +++ b/controllers/account/controllers/accountv2_test.go @@ -15,30 +15,11 @@ package controllers import ( - "context" - "os" - "path/filepath" "testing" - "time" "github.com/labring/sealos/controllers/pkg/database" - "github.com/labring/sealos/controllers/pkg/database/mongo" - - "golang.org/x/sync/errgroup" - "github.com/labring/sealos/controllers/pkg/database/cockroach" - "github.com/labring/sealos/controllers/pkg/utils/logger" - - "sigs.k8s.io/controller-runtime/pkg/client" - - "k8s.io/client-go/tools/clientcmd" - - "k8s.io/apimachinery/pkg/runtime" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - - accountv1 "github.com/labring/sealos/controllers/account/api/v1" - "github.com/labring/sealos/controllers/pkg/types" ) @@ -47,202 +28,6 @@ var ( testV2LocalDBURI = "" ) -type testConfig struct { - RegionID string - V1dbURI string - V2GlobalDBURI string - V2LocalDBURI string - Kubeconfig string -} - -var RegionsConfig = []testConfig{} - -func mkdirs(dirs ...string) error { - for _, dir := range dirs { - err := os.MkdirAll(dir, 0755) - if err != nil { - return err - } - } - return nil -} - -func TestAccount_V1ToV2(t *testing.T) { - for i := range RegionsConfig { - os.Unsetenv("LOCAL_REGION") - err := os.Setenv("LOCAL_REGION", RegionsConfig[i].RegionID) - if err != nil { - t.Fatalf("failed to set env: %v", err) - } - err = mkdirs(filepath.Join("transferv1tov2", "null_user_record", RegionsConfig[i].RegionID), - filepath.Join("transferv1tov2", "transfer_account_v1", RegionsConfig[i].RegionID), - filepath.Join("transferv1tov2", "transfer_account_v1_exist", RegionsConfig[i].RegionID)) - if err != nil { - t.Fatalf("failed to create dir: %v", err) - } - scheme := runtime.NewScheme() - utilruntime.Must(accountv1.AddToScheme(scheme)) - config, err := clientcmd.BuildConfigFromFlags("", RegionsConfig[i].Kubeconfig) - if err != nil { - t.Fatalf("Error building kubeconfig: %v\n", err) - } - clt, err := client.New(config, client.Options{Scheme: scheme}) - if err != nil { - t.Fatalf("failed to new client: %v", err) - } - accounts := &accountv1.AccountList{} - err = clt.List(context.Background(), accounts) - if err != nil { - t.Fatalf("failed to get account: %v", err) - } - t.Logf("success get region account len: %d, startTime: %s", len(accounts.Items), time.Now().UTC().Format("2006-01-02 15:04:05")) - accountItf, err := database.NewAccountV2(RegionsConfig[i].V2GlobalDBURI, RegionsConfig[i].V2LocalDBURI) - if err != nil { - t.Fatalf("failed to new account : %v", err) - } - defer func() { - if err := accountItf.Close(); err != nil { - t.Errorf("failed close connection: %v", err) - } - }() - wg, ctx := errgroup.WithContext(context.Background()) - wg.SetLimit(100) - for _, a := range accounts.Items { - account := a - wg.Go(func() error { - createAccount := &types.Account{ - EncryptBalance: *account.Status.EncryptBalance, - EncryptDeductionBalance: *account.Status.EncryptDeductionBalance, - Balance: account.Status.Balance, - DeductionBalance: account.Status.DeductionBalance, - CreatedAt: account.CreationTimestamp.Time, - CreateRegionID: RegionsConfig[i].RegionID, - ActivityBonus: account.Status.ActivityBonus, - } - _, err := accountItf.TransferAccountV1(account.Name, createAccount) - if err != nil { - logger.Error("failed to create account %s: %v", account.Name, err) - if err = accountItf.CreateErrorAccountCreate(createAccount, account.Name, err.Error()); err != nil { - logger.Error("failed to create err msg %s: %v", account.Name, err) - ctx.Done() - } - return err - } - //t.Logf("success create account %s", account.Name) - return nil - }) - } - if err := wg.Wait(); err != nil { - t.Fatalf("failed to create account: %v", err) - } - } -} - -func TestConvertPayment_V1ToV2(t *testing.T) { - for i := range RegionsConfig { - os.Unsetenv("LOCAL_REGION") - err := os.Setenv("LOCAL_REGION", RegionsConfig[i].RegionID) - if err != nil { - t.Fatalf("failed to set env: %v", err) - } - accountV2, err := database.NewAccountV2(RegionsConfig[i].V2GlobalDBURI, RegionsConfig[i].V2LocalDBURI) - if err != nil { - t.Fatalf("failed to new account : %v", err) - } - defer func() { - if err := accountV2.Close(); err != nil { - t.Errorf("failed close connection: %v", err) - } - }() - accountV1, err := mongo.NewMongoInterface(context.Background(), RegionsConfig[i].V1dbURI) - if err != nil { - t.Fatalf("failed to new account : %v", err) - } - defer func() { - if err := accountV1.Disconnect(context.Background()); err != nil { - t.Errorf("failed close connection: %v", err) - } - }() - billings, err := accountV1.GetAllPayment() - if err != nil { - t.Fatalf("failed to get billing: %v", err) - } - eg := errgroup.Group{} - eg.SetLimit(100) - - for i := range billings { - bill := billings[i] - eg.Go(func() error { - payment := types.Payment{ - ID: bill.OrderID, - PaymentRaw: types.PaymentRaw{ - CreatedAt: bill.Time, - Amount: bill.Payment.Amount, - RegionUserOwner: bill.Owner, - Method: bill.Payment.Method, - CodeURL: bill.Payment.CodeURL, - TradeNO: bill.Payment.TradeNO, - Gift: bill.Amount - bill.Payment.Amount, - }, - } - err = accountV2.SavePayment(&payment) - if err != nil { - //logger.Error("failed to create payment %s: %v", payment.CrName, err) - if err2 := accountV2.CreateErrorPaymentCreate(payment, err.Error()); err2 != nil { - logger.Error("failed to create err msg %s: %v", payment.ID, err2) - } - } - //logger.Info("success get payment %s", bill) - return nil - }) - } - if err := eg.Wait(); err != nil { - t.Fatalf("failed to wait create payment: %v", err) - return - } - } - t.Logf("success convert payment") -} - -//func TestAccountConvert(t *testing.T) { -// scheme := runtime.NewScheme() -// utilruntime.Must(accountv1.AddToScheme(scheme)) -// config, err := clientcmd.BuildConfigFromFlags("", testConfig) -// if err != nil { -// t.Fatalf("Error building kubeconfig: %v\n", err) -// } -// clt, err := client.New(config, client.Options{Scheme: scheme}) -// if err != nil { -// t.Fatalf("failed to new client: %v", err) -// } -// accounts := &accountv1.AccountList{} -// err = clt.List(context.Background(), accounts) -// if err != nil { -// t.Fatalf("failed to get account: %v", err) -// } -// for _, a := range accounts.Items { -// account := a -// if account.Status.EncryptBalance != nil || account.Status.EncryptDeductionBalance != nil { -// if account.Status.EncryptBalance != nil && account.Status.EncryptDeductionBalance != nil { -// continue -// } -// t.Logf("account %s already convert", account.Name) -// continue -// } -// accountCopy := &accountv1.Account{} -// err = json.Unmarshal([]byte(account.Annotations[v1.LastAppliedConfigAnnotation]), accountCopy) -// if err != nil { -// t.Fatalf("failed to unmarshal account %s: %v", account.Name, err) -// } -// account.Status = accountCopy.Status -// err = clt.Status().Update(context.Background(), &account) -// if err != nil { -// t.Fatalf("failed to update account %s: %v", account.Name, err) -// } -// t.Logf("success updata status account %s: %+v", account.Name, account.Status) -// } -//} - func TestAccountV2_CreateAccount(t *testing.T) { account, err := database.NewAccountV2(testV2GlobalDBURI, testV2LocalDBURI) if err != nil { diff --git a/controllers/account/controllers/billinginfoquery_controller.go b/controllers/account/controllers/billinginfoquery_controller.go deleted file mode 100644 index 98bff636665..00000000000 --- a/controllers/account/controllers/billinginfoquery_controller.go +++ /dev/null @@ -1,195 +0,0 @@ -/* -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" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/labring/sealos/controllers/pkg/types" - - userv1 "github.com/labring/sealos/controllers/user/api/v1" - - "github.com/go-logr/logr" - - "github.com/labring/sealos/controllers/pkg/resources" - - "github.com/labring/sealos/controllers/pkg/database" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - accountv1 "github.com/labring/sealos/controllers/account/api/v1" -) - -// BillingInfoQueryReconciler reconciles a BillingInfoQuery object -type BillingInfoQueryReconciler struct { - client.Client - logr.Logger - Scheme *runtime.Scheme - DBClient database.Account - //TODO init - AccountV2 database.AccountV2 - AccountSystemNamespace string - Properties *resources.PropertyTypeLS - propertiesQuery []accountv1.PropertyQuery - Activities types.Activities - DefaultDiscount types.RechargeDiscount - QueryFuncMap map[string]func(context.Context, ctrl.Request, *accountv1.BillingInfoQuery) (string, error) -} - -//+kubebuilder:rbac:groups=account.sealos.io,resources=billinginfoqueries,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=account.sealos.io,resources=billinginfoqueries/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=account.sealos.io,resources=billinginfoqueries/finalizers,verbs=update - -func (r *BillingInfoQueryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctl ctrl.Result, err error) { - query := &accountv1.BillingInfoQuery{} - if err = r.Get(ctx, req.NamespacedName, query); err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - if err := r.deleteTimeoutQueryCRList(ctx); err != nil { - r.Logger.Error(err, "delete timeout query error") - } - r.reconcileQuery(ctx, req, query) - - return ctrl.Result{}, r.Status().Update(ctx, query) -} - -func (r *BillingInfoQueryReconciler) reconcileQuery(ctx context.Context, req ctrl.Request, query *accountv1.BillingInfoQuery) { - var err error - queryFunc := r.QueryFuncMap[strings.ToLower(query.Spec.QueryType)] - if queryFunc == nil { - query.Status.Status = accountv1.Failed - query.Status.StatusDetails = fmt.Sprintf("query type %s not supported", query.Spec.QueryType) - return - } - if query.Status.Result, err = queryFunc(ctx, req, query); err != nil { - query.Status.Status = accountv1.Failed - query.Status.StatusDetails = err.Error() - return - } - query.Status.Status = accountv1.Completed -} - -// timeout five minutes -func (r *BillingInfoQueryReconciler) deleteTimeoutQueryCRList(ctx context.Context) error { - queryList := &accountv1.BillingInfoQueryList{} - err := r.List(ctx, queryList) - if err != nil { - return err - } - - for _, query := range queryList.Items { - if time.Since(query.CreationTimestamp.Time) > time.Minute*6 { - if err := r.Delete(ctx, &query); client.IgnoreNotFound(err) != nil { - return fmt.Errorf("delete billinginfoquery error: %v", err) - } - } - } - return nil -} - -func (r *BillingInfoQueryReconciler) NamespacesHistoryQuery(ctx context.Context, req ctrl.Request, _ *accountv1.BillingInfoQuery) (result string, err 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) - } - var nsList []string - owner, ok := user.GetAnnotations()[userv1.UserAnnotationOwnerKey] - if ok { - nsList, err = r.DBClient.GetBillingHistoryNamespaces(nil, nil, int(accountv1.QueryAllType), owner) - if err != nil { - return "", fmt.Errorf("get billing history namespaces failed: %w", err) - } - } - - data, err := json.Marshal(nsList) - if err != nil { - return "", fmt.Errorf("marshal billing history namespaces failed: %w", err) - } - return string(data), nil -} - -func (r *BillingInfoQueryReconciler) PropertiesQuery(_ context.Context, _ ctrl.Request, _ *accountv1.BillingInfoQuery) (result string, err error) { - data, err := json.Marshal(r.propertiesQuery) - if err != nil { - return "", fmt.Errorf("marshal properties query failed: %w", err) - } - return string(data), nil -} - -func (r *BillingInfoQueryReconciler) AppTypeQuery(_ context.Context, _ ctrl.Request, _ *accountv1.BillingInfoQuery) (result string, err error) { - var appTypeList []string - for appType := range resources.AppType { - appTypeList = append(appTypeList, appType) - } - data, err := json.Marshal(appTypeList) - if err != nil { - return "", fmt.Errorf("marshal type query failed: %w", err) - } - return string(data), nil -} - -func (r *BillingInfoQueryReconciler) RechargeQuery(_ context.Context, _ ctrl.Request, billingInfoQuery *accountv1.BillingInfoQuery) (result string, err error) { - //TODO get owner - userDiscount, err := r.AccountV2.GetUserAccountRechargeDiscount(&types.UserQueryOpts{Owner: getUsername(billingInfoQuery.Namespace)}) - if err != nil { - return "", fmt.Errorf("parse user activities failed: %w", err) - } - if userDiscount == nil || len(userDiscount.DiscountRates) == 0 || len(userDiscount.DiscountSteps) == 0 { - userDiscount = &r.DefaultDiscount - } - data, err := json.Marshal(userDiscount) - if err != nil { - return "", fmt.Errorf("marshal recharge discount failed: %w", err) - } - return string(data), nil -} - -func (r *BillingInfoQueryReconciler) ConvertPropertiesToQuery() error { - r.propertiesQuery = make([]accountv1.PropertyQuery, 0) - for _, types := range r.Properties.Types { - property := accountv1.PropertyQuery{ - Name: types.Name, - UnitPrice: types.UnitPrice, - Unit: types.UnitString, - Alias: types.Alias, - } - r.propertiesQuery = append(r.propertiesQuery, property) - } - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *BillingInfoQueryReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.Logger = ctrl.Log.WithName("controllers").WithName("BillingInfoQuery") - if err := r.ConvertPropertiesToQuery(); err != nil { - return fmt.Errorf("convert properties to query failed: %w", err) - } - r.QueryFuncMap = make(map[string]func(context.Context, ctrl.Request, *accountv1.BillingInfoQuery) (string, error), 3) - r.QueryFuncMap[strings.ToLower(accountv1.QueryTypeNamespacesHistory)] = r.NamespacesHistoryQuery - r.QueryFuncMap[strings.ToLower(accountv1.QueryTypeProperties)] = r.PropertiesQuery - r.QueryFuncMap[strings.ToLower(accountv1.QueryTypeAppType)] = r.AppTypeQuery - r.QueryFuncMap[strings.ToLower(accountv1.QueryTypeRecharge)] = r.RechargeQuery - return ctrl.NewControllerManagedBy(mgr). - For(&accountv1.BillingInfoQuery{}). - Complete(r) -} diff --git a/controllers/account/controllers/billingrecordquery_controller.go b/controllers/account/controllers/billingrecordquery_controller.go deleted file mode 100644 index 1a5b86f47bd..00000000000 --- a/controllers/account/controllers/billingrecordquery_controller.go +++ /dev/null @@ -1,169 +0,0 @@ -/* -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" - "strconv" - "time" - - "github.com/labring/sealos/controllers/pkg/database/mongo" - - "github.com/go-logr/logr" - - accountv1 "github.com/labring/sealos/controllers/account/api/v1" - "github.com/labring/sealos/controllers/pkg/database" - "github.com/labring/sealos/controllers/pkg/resources" - "github.com/labring/sealos/controllers/pkg/utils/env" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller" - "sigs.k8s.io/controller-runtime/pkg/handler" - "sigs.k8s.io/controller-runtime/pkg/log" -) - -// BillingRecordQueryReconciler reconciles a BillingRecordQuery object -type BillingRecordQueryReconciler struct { - client.Client - Scheme *runtime.Scheme - Logger logr.Logger - MongoDBURI string - AccountSystemNamespace string -} - -//+kubebuilder:rbac:groups=account.sealos.io,resources=billingrecordqueries,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=account.sealos.io,resources=billingrecordqueries/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=account.sealos.io,resources=billingrecordqueries/finalizers,verbs=update -//+kubebuilder:rbac:groups=account.sealos.io,resources=pricequeries,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=account.sealos.io,resources=pricequeries/status,verbs=get;update;patch -//+kubebuilder:rbac:groups=account.sealos.io,resources=pricequeries/finalizers,verbs=update -//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch -//+kubebuilder:rbac:groups="",resources=configmaps/status,verbs=get - -// 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 BillingRecordQuery 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 *BillingRecordQueryReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - - dbCtx := context.Background() - dbClient, err := mongo.NewMongoInterface(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) - } - }() - - priceQuery := &accountv1.PriceQuery{} - err = r.Get(ctx, req.NamespacedName, priceQuery) - if err == nil { - return r.ReconcilePriceQuery(ctx, priceQuery) - } else if client.IgnoreNotFound(err) != nil { - return ctrl.Result{}, err - } - - billingRecordQuery := &accountv1.BillingRecordQuery{} - err = r.Get(ctx, req.NamespacedName, billingRecordQuery) - if err != nil { - return ctrl.Result{}, client.IgnoreNotFound(err) - } - - if time.Since(billingRecordQuery.CreationTimestamp.Time) > (3 * time.Minute) { - err = r.Delete(ctx, billingRecordQuery) - return ctrl.Result{}, err - } - //check options - - if err = CheckOpts(billingRecordQuery); err != nil { - //TODO update status - return ctrl.Result{}, err - } - - err = dbClient.QueryBillingRecords(billingRecordQuery, getUsername(billingRecordQuery.Namespace)) - if err != nil { - r.Logger.Error(err, "query billing records failed") - return ctrl.Result{Requeue: true}, err - } - if err = r.Status().Update(ctx, billingRecordQuery); err != nil { - r.Logger.Error(err, "update billing record query status failed") - return ctrl.Result{Requeue: true}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: time.Minute * 4}, nil -} - -func CheckOpts(billingRecordQuery *accountv1.BillingRecordQuery) error { - if billingRecordQuery.Spec.Page < 1 || billingRecordQuery.Spec.PageSize < 1 { - return fmt.Errorf("page and pageSize must be greater than 0") - } - return nil -} - -// SetupWithManager sets up the controller with the Manager. -func (r *BillingRecordQueryReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller.Options) error { - if r.MongoDBURI = os.Getenv(database.MongoURI); r.MongoDBURI == "" { - return fmt.Errorf("env %s is empty", database.MongoURI) - } - r.Logger = log.Log.WithName("billingrecordquery-controller") - r.AccountSystemNamespace = env.GetEnvWithDefault(ACCOUNTNAMESPACEENV, DEFAULTACCOUNTNAMESPACE) - return ctrl.NewControllerManagedBy(mgr). - For(&accountv1.BillingRecordQuery{}). - Watches(&accountv1.PriceQuery{}, &handler.EnqueueRequestForObject{}). - WithOptions(rateOpts). - Complete(r) -} - -func (r *BillingRecordQueryReconciler) ReconcilePriceQuery(ctx context.Context, priceQuery *accountv1.PriceQuery) (ctrl.Result, error) { - // TODO query price - if time.Since(priceQuery.CreationTimestamp.Time) > (3 * time.Minute) { - err := r.Delete(ctx, priceQuery) - return ctrl.Result{}, err - } - priceQuery.Status.BillingRecords = make([]accountv1.BillingRecord, 0) - for _, property := range resources.DefaultPropertyTypeLS.Types { - displayName, displayPrice := property.Name, property.UnitPrice - if resources.IsGpuResource(property.Name) && property.Alias != "" { - displayName = string(resources.NewGpuResource(property.Alias)) - } - if property.ViewPrice > 0 { - displayPrice = property.ViewPrice - } - priceQuery.Status.BillingRecords = append(priceQuery.Status.BillingRecords, accountv1.BillingRecord{ - ResourceType: displayName, - Price: strconv.FormatFloat(displayPrice, 'f', -1, 64), - }) - } - if err := r.Status().Update(ctx, priceQuery); err != nil { - r.Logger.Error(err, "update price query status failed") - return ctrl.Result{Requeue: true}, err - } - return ctrl.Result{Requeue: true, RequeueAfter: time.Minute * 4}, nil -} diff --git a/controllers/account/controllers/cache/cache.go b/controllers/account/controllers/cache/cache.go index 4498c2d0d08..c823e954700 100644 --- a/controllers/account/controllers/cache/cache.go +++ b/controllers/account/controllers/cache/cache.go @@ -27,10 +27,6 @@ import ( ) func SetupCache(mgr ctrl.Manager) error { - account := &accountv1.Account{} - accountNameFunc := func(obj client.Object) []string { - return []string{obj.(*accountv1.Account).Name} - } ns := &corev1.Namespace{} nsNameFunc := func(obj client.Object) []string { return []string{obj.(*corev1.Namespace).Name} @@ -45,8 +41,7 @@ func SetupCache(mgr ctrl.Manager) error { extractValue client.IndexerFunc }{ {ns, accountv1.Name, nsNameFunc}, - {ns, accountv1.Owner, nsOwnerFunc}, - {account, accountv1.Name, accountNameFunc}} { + {ns, accountv1.Owner, nsOwnerFunc}} { if err := mgr.GetFieldIndexer().IndexField(context.TODO(), idx.obj, idx.field, idx.extractValue); err != nil { return err } diff --git a/controllers/account/controllers/debt_controller.go b/controllers/account/controllers/debt_controller.go index 91fa144d317..8a3de2cb7fc 100644 --- a/controllers/account/controllers/debt_controller.go +++ b/controllers/account/controllers/debt_controller.go @@ -783,7 +783,7 @@ func (r *DebtReconciler) SetupWithManager(mgr ctrl.Manager, rateOpts controller. r.Logger.Info("set config", "DebtConfig", DebtConfig, "DebtDetectionCycle", r.DebtDetectionCycle, "accountSystemNamespace", r.accountSystemNamespace) return ctrl.NewControllerManagedBy(mgr). - For(&userv1.User{}, builder.WithPredicates(predicate.And(UserOwnerPredicate{}))). + For(&userv1.User{}, builder.WithPredicates(predicate.And(UserOwnerPredicate{})), builder.OnlyMetadata). Watches(&accountv1.Payment{}, &handler.EnqueueRequestForObject{}). WithOptions(rateOpts). Complete(r) diff --git a/controllers/account/controllers/namespacebillinghistory_controller.go b/controllers/account/controllers/namespacebillinghistory_controller.go deleted file mode 100644 index 36abb44ff11..00000000000 --- a/controllers/account/controllers/namespacebillinghistory_controller.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -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" - - "github.com/labring/sealos/controllers/pkg/database/mongo" - - userv1 "github.com/labring/sealos/controllers/user/api/v1" - - "github.com/go-logr/logr" - "sigs.k8s.io/controller-runtime/pkg/log" - - "github.com/labring/sealos/controllers/pkg/database" - - "k8s.io/apimachinery/pkg/runtime" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - - accountv1 "github.com/labring/sealos/controllers/account/api/v1" -) - -// 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 := mongo.NewMongoInterface(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.Account, 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/controllers/payment_controller.go b/controllers/account/controllers/payment_controller.go index 5f0ab3223b9..1a016bc44a7 100644 --- a/controllers/account/controllers/payment_controller.go +++ b/controllers/account/controllers/payment_controller.go @@ -23,6 +23,8 @@ import ( "sync" "time" + "github.com/google/uuid" + "sigs.k8s.io/controller-runtime/pkg/manager" pkgtypes "github.com/labring/sealos/controllers/pkg/types" @@ -47,6 +49,8 @@ type PaymentReconciler struct { Logger logr.Logger reconcileDuration time.Duration createDuration time.Duration + accountConfig pkgtypes.AccountConfig + userLock map[uuid.UUID]*sync.Mutex domain string } @@ -69,13 +73,14 @@ const ( //+kubebuilder:rbac:groups=account.sealos.io,resources=payments/finalizers,verbs=update // SetupWithManager sets up the controller with the Manager. -func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error { +func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) (err error) { const controllerName = "payment_controller" r.Logger = ctrl.Log.WithName(controllerName) r.Logger.V(1).Info("init reconcile controller payment") r.domain = os.Getenv("DOMAIN") r.reconcileDuration = defaultReconcileDuration r.createDuration = defaultCreateDuration + r.userLock = make(map[uuid.UUID]*sync.Mutex) if duration := os.Getenv(EnvPaymentReconcileDuration); duration != "" { reconcileDuration, err := time.ParseDuration(duration) if err == nil { @@ -88,6 +93,14 @@ func (r *PaymentReconciler) SetupWithManager(mgr ctrl.Manager) error { r.createDuration = createDuration } } + r.accountConfig, err = r.Account.AccountV2.GetAccountConfig() + if err != nil { + return fmt.Errorf("get account config failed: %w", err) + } + if len(r.accountConfig.DefaultDiscountSteps) == 0 { + r.Logger.Info("default discount steps is empty, use default value") + } + r.Logger.V(1).Info("account config", "config", r.accountConfig) r.Logger.V(1).Info("reconcile duration", "reconcileDuration", r.reconcileDuration, "createDuration", r.createDuration) if err := mgr.Add(r); err != nil { return fmt.Errorf("add payment controller failed: %w", err) @@ -142,22 +155,6 @@ func (r *PaymentReconciler) reconcilePayments(_ context.Context) (errs []error) } func (r *PaymentReconciler) reconcileCreatePayments(ctx context.Context) (errs []error) { - //paymentList := &accountv1.PaymentList{} - //listOpts := &client.ListOptions{ - // FieldSelector: fields.OneTermEqualSelector("status.tradeNO", ""), - //} - //// handler old payment - //err := r.Client.List(context.Background(), paymentList, listOpts) - //if err != nil { - // errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) - // return - //} - //for _, payment := range paymentList.Items { - // if err := r.reconcileNewPayment(&payment); err != nil { - // errs = append(errs, fmt.Errorf("reconcile payment failed: payment: %s, user: %s, err: %w", payment.Name, payment.Spec.UserID, err)) - // } - //} - // watch new payment watcher, err := r.WatchClient.Watch(context.Background(), &accountv1.PaymentList{}, &client.ListOptions{}) if err != nil { errs = append(errs, fmt.Errorf("watch payment failed: %w", err)) @@ -211,20 +208,34 @@ func (r *PaymentReconciler) reconcilePayment(payment *accountv1.Payment) error { if err != nil { return fmt.Errorf("get user failed: %w", err) } + if r.userLock[user.UID] == nil { + r.userLock[user.UID] = &sync.Mutex{} + } + r.userLock[user.UID].Lock() + defer r.userLock[user.UID].Unlock() + userDiscount, err := r.Account.AccountV2.GetUserRechargeDiscount(&pkgtypes.UserQueryOpts{ID: payment.Spec.UserID}) + if err != nil { + return fmt.Errorf("get user discount failed: %w", err) + } //1¥ = 100WechatPayAmount; 1 WechatPayAmount = 10000 SealosAmount payAmount := orderAmount * 10000 - gift := getAmountWithDiscount(payAmount, r.Account.DefaultDiscount) + isFirstRecharge, gift := getFirstRechargeDiscount(payAmount, userDiscount) + paymentRaw := pkgtypes.PaymentRaw{ + UserUID: user.UID, + Amount: payAmount, + Gift: gift, + CreatedAt: payment.CreationTimestamp.Time, + RegionUserOwner: getUsername(payment.Namespace), + Method: payment.Spec.PaymentMethod, + TradeNO: payment.Status.TradeNO, + CodeURL: payment.Status.CodeURL, + } + if isFirstRecharge { + paymentRaw.ActivityType = pkgtypes.ActivityTypeFirstRecharge + } + if err = r.Account.AccountV2.Payment(&pkgtypes.Payment{ - PaymentRaw: pkgtypes.PaymentRaw{ - UserUID: user.UID, - Amount: payAmount, - Gift: gift, - CreatedAt: payment.CreationTimestamp.Time, - RegionUserOwner: getUsername(payment.Namespace), - Method: payment.Spec.PaymentMethod, - TradeNO: payment.Status.TradeNO, - CodeURL: payment.Status.CodeURL, - }, + PaymentRaw: paymentRaw, }); err != nil { return fmt.Errorf("payment failed: %w", err) } diff --git a/controllers/account/controllers/transfer_controller.go b/controllers/account/controllers/transfer_controller.go deleted file mode 100644 index 705c62b8330..00000000000 --- a/controllers/account/controllers/transfer_controller.go +++ /dev/null @@ -1,256 +0,0 @@ -/* -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" -// "strconv" -// "time" -// -// "github.com/labring/sealos/controllers/pkg/common" -// -// "github.com/labring/sealos/controllers/pkg/resources" -// -// "github.com/labring/sealos/controllers/pkg/database" -// -// "github.com/labring/sealos/controllers/pkg/crypto" -// -// "github.com/go-logr/logr" -// gonanoid "github.com/matoous/go-nanoid/v2" -// metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -// "sigs.k8s.io/controller-runtime/pkg/builder" -// -// v1 "github.com/labring/sealos/controllers/pkg/notification/api/v1" -// -// "k8s.io/apimachinery/pkg/runtime" -// ctrl "sigs.k8s.io/controller-runtime" -// "sigs.k8s.io/controller-runtime/pkg/client" -// -// accountv1 "github.com/labring/sealos/controllers/account/api/v1" -//) -// -//var MinBalance int64 = 10_000000 -// -//// TransferReconciler reconciles a Transfer object -//type TransferReconciler struct { -// Logger logr.Logger -// client.Client -// Scheme *runtime.Scheme -// AccountSystemNamespace string -// DBClient database.Account -//} -// -////TODO add user, account role -////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts,verbs=get;list;watch;create -////+kubebuilder:rbac:groups=account.sealos.io,resources=accounts/status,verbs=get -////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers,verbs=get;list;watch;create;update;patch;delete -////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/status,verbs=get;update;patch -////+kubebuilder:rbac:groups=account.sealos.io,resources=transfers/finalizers,verbs=update -////+kubebuilder:rbac:groups=notification.sealos.io,resources=notifications,verbs=get;list;watch;create;update;patch;delete -// -//// 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 Transfer 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 *TransferReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { -// transfer := accountv1.Transfer{} -// if err := r.Get(ctx, req.NamespacedName, &transfer); err != nil { -// return ctrl.Result{}, client.IgnoreNotFound(err) -// } -// transfer.Spec.From = getUsername(transfer.Namespace) -// if time.Since(transfer.CreationTimestamp.Time) > time.Minute*3 { -// return ctrl.Result{}, r.Delete(ctx, &transfer) -// } -// //TODO Error rollback required -// pipeLine := []func(ctx context.Context, transfer *accountv1.Transfer) error{ -// r.check, -// r.transferSaver, -// r.transferAccount, -// } -// for _, f := range pipeLine { -// if err := f(ctx, &transfer); err != nil { -// transfer.Status.Reason = err.Error() -// transfer.Status.Progress = accountv1.TransferStateFailed -// break -// } -// } -// if transfer.Status.Progress != accountv1.TransferStateFailed { -// transfer.Status.Progress = accountv1.TransferStateCompleted -// } -// if err := r.Status().Update(ctx, &transfer); err != nil { -// return ctrl.Result{}, fmt.Errorf("update transfer status failed: %w", err) -// } -// return ctrl.Result{RequeueAfter: 3 * time.Minute}, nil -//} -// -//// SetupWithManager sets up the controller with the Manager. -//func (r *TransferReconciler) SetupWithManager(mgr ctrl.Manager) error { -// r.AccountSystemNamespace = os.Getenv(ACCOUNTNAMESPACEENV) -// if r.AccountSystemNamespace == "" { -// r.AccountSystemNamespace = DEFAULTACCOUNTNAMESPACE -// } -// r.Logger = ctrl.Log.WithName("transfer-controller") -// if m := os.Getenv("TRANSFERMINBALANCE"); m != "" { -// minBalance, err := strconv.ParseInt(m, 10, 64) -// if err != nil { -// r.Logger.Error(err, "parse min balance failed") -// } else { -// MinBalance = minBalance -// } -// } -// return ctrl.NewControllerManagedBy(mgr). -// For(&accountv1.Transfer{}, builder.WithPredicates(OnlyCreatePredicate{})). -// Complete(r) -//} -// -//func (r *TransferReconciler) transferSaver(ctx context.Context, transfer *accountv1.Transfer) error { -// idOut, err := gonanoid.New(12) -// if err != nil { -// return fmt.Errorf("create id failed: %w", err) -// } -// idIn, err := gonanoid.New(12) -// if err != nil { -// return fmt.Errorf("create id failed: %w", err) -// } -// err = r.DBClient.SaveBillings(&resources.Billing{ -// OrderID: idOut, -// Amount: transfer.Spec.Amount, -// Owner: getUsername(transfer.Namespace), -// Type: accountv1.TransferOut, -// Namespace: transfer.Namespace, -// Time: transfer.CreationTimestamp.Time, -// Transfer: &resources.Transfer{ -// To: transfer.Spec.To, -// Amount: transfer.Spec.Amount, -// }, -// }, &resources.Billing{ -// OrderID: idIn, -// Amount: transfer.Spec.Amount, -// Owner: getUsername(transfer.Spec.To), -// Type: accountv1.TransferIn, -// Namespace: transfer.Namespace, -// Time: transfer.CreationTimestamp.Time, -// Transfer: &resources.Transfer{ -// From: transfer.Spec.From, -// Amount: transfer.Spec.Amount, -// }, -// }) -// if err != nil { -// return fmt.Errorf("save billing failed: %w", err) -// } -// if err = r.sendNotice(ctx, transfer.Namespace, transfer.Spec.To, transfer.Spec.Amount, accountv1.TransferOut); err != nil { -// r.Logger.Error(err, "send notice failed") -// } -// if err := r.sendNotice(ctx, transfer.Spec.To, transfer.Namespace, transfer.Spec.Amount, accountv1.TransferIn); err != nil { -// r.Logger.Error(err, "send notice failed") -// } -// return nil -//} -// -//func (r *TransferReconciler) transferAccount(ctx context.Context, transfer *accountv1.Transfer) error { -// from, to := transfer.Namespace, transfer.Spec.To -// var fromAccount, toAccount accountv1.Account -// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(from)}, &fromAccount) != nil { -// return fmt.Errorf("owner %s account not found", from) -// } -// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &toAccount) != nil { -// return fmt.Errorf("owner %s account not found", to) -// } -// balance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptBalance) -// deductionBalance, _ := crypto.DecryptInt64(*fromAccount.Status.EncryptDeductionBalance) -// // check balance is enough ( balance - deductionBalance - transferAmount - MinBalance - ActivityBonus) activity give amount not included -// if balance < deductionBalance+transfer.Spec.Amount+MinBalance+fromAccount.Status.ActivityBonus { -// return fmt.Errorf("balance not enough") -// } -// if r.Get(ctx, client.ObjectKey{Namespace: r.AccountSystemNamespace, Name: getUsername(to)}, &accountv1.Account{}) != nil { -// return fmt.Errorf("user %s account not found", transfer.Spec.To) -// } -// err := crypto.RechargeBalance(toAccount.Status.EncryptBalance, transfer.Spec.Amount) -// if err != nil { -// return err -// } -// err = crypto.DeductBalance(fromAccount.Status.EncryptBalance, transfer.Spec.Amount) -// if err != nil { -// return err -// } -// if err = SyncAccountStatus(ctx, r.Client, &toAccount); err != nil { -// return fmt.Errorf("sync account status failed: %w", err) -// } -// if err = SyncAccountStatus(ctx, r.Client, &fromAccount); err != nil { -// return fmt.Errorf("sync account status failed: %w", err) -// } -// return nil -//} -// -//const ( -// TransferInNotification = `You have a new transfer from %s, amount: %d` -// TransferOutNotification = `You have a new transfer to %s, amount: %d` -//) -// -//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 common.Type) error { -// now := time.Now().UTC().Unix() -// ntf := v1.Notification{ -// ObjectMeta: metav1.ObjectMeta{ -// Name: "transfer-notice-" + strconv.FormatInt(now, 10), -// Namespace: GetUserNamespace(getUsername(namespace)), -// }, -// Spec: v1.NotificationSpec{ -// Title: "Transfer Notice", -// Message: fmt.Sprintf(transferNotification[_type], GetUserNamespace(getUsername(user)), convertAmount(amount)), -// From: "Account-System", -// Timestamp: now, -// Importance: v1.Low, -// }, -// } -// return r.Create(ctx, &ntf) -//} -// -//// Convert amount 1¥:1000000 -//func convertAmount(amount int64) int64 { -// return amount / 1_000_000 -//} -// -//func (r *TransferReconciler) check(_ context.Context, transfer *accountv1.Transfer) error { -// if transfer.Spec.Amount <= 0 { -// return fmt.Errorf("amount must be greater than 0") -// } -// if transfer.Status.Progress == accountv1.TransferStateFailed { -// return fmt.Errorf(transfer.Status.Reason) -// } -// if transfer.Status.Progress == accountv1.TransferStateCompleted { -// return fmt.Errorf("transfer already completed") -// } -// from := transfer.Namespace -// to := transfer.Spec.To -// if getUsername(from) == getUsername(to) { -// return fmt.Errorf("can not transfer to self") -// } -// return nil -//} diff --git a/controllers/account/deploy/manifests/deploy.yaml b/controllers/account/deploy/manifests/deploy.yaml index 3ed178e2e76..6ae9808efe5 100644 --- a/controllers/account/deploy/manifests/deploy.yaml +++ b/controllers/account/deploy/manifests/deploy.yaml @@ -21,315 +21,10 @@ metadata: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.8.0 - creationTimestamp: null - name: accounts.account.sealos.io -spec: - group: account.sealos.io - names: - kind: Account - listKind: AccountList - plural: accounts - singular: account - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Account is the Schema for the accounts 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: AccountSpec defines the desired state of Account - type: object - status: - description: AccountStatus defines the observed state of Account - properties: - activityBonus: - description: 'ActivityBonus: for demonstration purposes only and does - not participate in calculation' - format: int64 - type: integer - balance: - description: Recharge amount - format: int64 - type: integer - chargeList: - description: delete in the future - items: - properties: - accountBalanceName: - type: string - balance: - format: int64 - type: integer - deductionAmount: - description: deduction info will Record in the Charge - format: int64 - type: integer - describe: - type: string - status: - type: string - time: - format: date-time - type: string - tradeNO: - type: string - type: object - type: array - deductionBalance: - description: Deduction amount - format: int64 - type: integer - encryptBalance: - description: EncryptBalance is to encrypt balance - type: string - encryptDeductionBalance: - description: EncryptDeductionBalance is to encrypt DeductionBalance - 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 - creationTimestamp: null - name: billinginfoqueries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: BillingInfoQuery - listKind: BillingInfoQueryList - plural: billinginfoqueries - singular: billinginfoquery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: BillingInfoQuery is the Schema for the billinginfoqueries 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: BillingInfoQuerySpec defines the desired state of BillingInfoQuery - properties: - args: - additionalProperties: - type: string - type: object - queryType: - type: string - required: - - queryType - type: object - status: - description: BillingInfoQueryStatus defines the observed state of BillingInfoQuery - properties: - result: - type: string - status: - type: string - statusDetails: - type: string - required: - - result - - status - - statusDetails - 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 - creationTimestamp: null - name: billingrecordqueries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: BillingRecordQuery - listKind: BillingRecordQueryList - plural: billingrecordqueries - singular: billingrecordquery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: BillingRecordQuery is the Schema for the billingrecordqueries - 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: BillingRecordQuerySpec defines the desired state of BillingRecordQuery - properties: - appType: - type: string - endTime: - format: date-time - type: string - namespace: - type: string - orderID: - type: string - page: - description: 'INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - Important: Run "make" to regenerate code after modifying this file' - type: integer - pageSize: - type: integer - startTime: - format: date-time - type: string - type: - type: integer - required: - - endTime - - page - - pageSize - - startTime - - type - type: object - status: - description: BillingRecordQueryStatus defines the observed state of BillingRecordQuery - properties: - deductionAmount: - format: int64 - type: integer - item: - items: - properties: - amount: - description: Amount = PaymentAmount + GiftAmount - format: int64 - type: integer - appType: - type: string - costs: - additionalProperties: - format: int64 - type: integer - type: object - name: - type: string - namespace: - type: string - order_id: - type: string - payment: - description: when Type = Recharge, PaymentAmount is the amount - of recharge - properties: - amount: - format: int64 - type: integer - type: object - time: - format: date-time - type: string - type: - type: integer - required: - - order_id - - time - - type - type: object - type: array - pageLength: - type: integer - rechargeAmount: - format: int64 - type: integer - status: - type: string - totalCount: - description: 'INSERT ADDITIONAL STATUS FIELD - define observed state - of cluster Important: Run "make" to regenerate code after modifying - this file' - type: integer - required: - - pageLength - - rechargeAmount - - status - - totalCount - 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: cert-manager.io/inject-ca-from: account-system/account-serving-cert - controller-gen.kubebuilder.io/version: v0.8.0 + controller-gen.kubebuilder.io/version: v0.14.0 name: debts.account.sealos.io spec: conversion: @@ -360,24 +55,29 @@ spec: description: Debt is the Schema for the debts 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' + 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' + 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: DebtSpec defines the desired state of Debt properties: - userName: - type: string userID: type: string + userName: + type: string type: object status: description: DebtStatus defines the observed state of Debt @@ -406,96 +106,12 @@ spec: 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 - 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 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: payments.account.sealos.io spec: group: account.sealos.io @@ -512,14 +128,19 @@ spec: description: Payment is the Schema for the payments 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' + 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' + 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 @@ -534,12 +155,12 @@ spec: default: wechat description: e.g. wechat, alipay, creditcard, etc. type: string - userID: - description: UserID is the user id who want to recharge - type: string userCR: description: UserCr is the user cr name who want to recharge type: string + userID: + description: UserID is the user id who want to recharge + type: string type: object status: description: PaymentStatus defines the observed state of Payment @@ -560,146 +181,6 @@ spec: 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 - creationTimestamp: null - name: pricequeries.account.sealos.io -spec: - group: account.sealos.io - names: - kind: PriceQuery - listKind: PriceQueryList - plural: pricequeries - singular: pricequery - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: PriceQuery is the Schema for the pricequeries 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: PriceQuerySpec defines the desired state of PriceQuery - type: object - status: - description: PriceQueryStatus defines the observed state of PriceQuery - properties: - billingRecords: - items: - properties: - discountType: - type: string - price: - type: string - resourceType: - type: string - required: - - price - - resourceType - type: object - type: array - 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 - creationTimestamp: null - name: transfers.account.sealos.io -spec: - group: account.sealos.io - names: - kind: Transfer - listKind: TransferList - plural: transfers - singular: transfer - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Transfer is the Schema for the transfers 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: TransferSpec defines the desired state of Transfer - properties: - amount: - format: int64 - minimum: 1000000 - type: integer - from: - type: string - to: - type: string - required: - - amount - - to - type: object - status: - description: TransferStatus defines the observed state of Transfer - properties: - progress: - type: integer - reason: - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] --- apiVersion: v1 kind: ServiceAccount @@ -748,7 +229,6 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: account-manager-role rules: - apiGroups: @@ -763,12 +243,6 @@ rules: - patch - update - watch -- apiGroups: - - "" - resources: - - configmaps/status - verbs: - - get - apiGroups: - "" resources: @@ -807,58 +281,6 @@ rules: - get - patch - update -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - billinginfoqueries/status - verbs: - - get - - patch - - update -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - billingrecordqueries/status - verbs: - - get - - patch - - update - apiGroups: - account.sealos.io resources: @@ -885,32 +307,6 @@ 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: @@ -937,32 +333,6 @@ rules: - get - patch - update -- apiGroups: - - account.sealos.io - resources: - - pricequeries - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - account.sealos.io - resources: - - pricequeries/finalizers - verbs: - - update -- apiGroups: - - account.sealos.io - resources: - - pricequeries/status - verbs: - - get - - patch - - update - apiGroups: - apps resources: @@ -1180,11 +550,10 @@ rules: resources: - users verbs: + - create - get - list - watch - - create - - patch --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole @@ -1257,6 +626,20 @@ subjects: apiVersion: v1 data: controller_manager_config.yaml: | + # 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. + apiVersion: controller-runtime.sigs.k8s.io/v1alpha1 kind: ControllerManagerConfig health: @@ -1321,6 +704,18 @@ spec: labels: control-plane: controller-manager spec: + affinity: + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - podAffinityTerm: + labelSelector: + matchExpressions: + - key: control-plane + operator: In + values: + - controller-manager + topologyKey: kubernetes.io/hostname + weight: 100 containers: - args: - --health-probe-bind-address=:8081 @@ -1405,18 +800,6 @@ spec: runAsNonRoot: true serviceAccountName: account-controller-manager terminationGracePeriodSeconds: 10 - affinity: - podAntiAffinity: - preferredDuringSchedulingIgnoredDuringExecution: - - weight: 100 - podAffinityTerm: - labelSelector: - matchExpressions: - - key: "control-plane" - operator: In - values: - - controller-manager - topologyKey: "kubernetes.io/hostname" volumes: - name: cert secret: diff --git a/controllers/account/main.go b/controllers/account/main.go index 291aa96c392..9f875848ac9 100644 --- a/controllers/account/main.go +++ b/controllers/account/main.go @@ -177,13 +177,6 @@ func main() { AccountV2: v2Account, CVMDBClient: cvmDBClient, } - billingInfoQueryReconciler := &controllers.BillingInfoQueryReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - DBClient: dbClient, - Properties: resources.DefaultPropertyTypeLS, - AccountV2: v2Account, - } activities, discountSteps, discountRatios, err := controllers.RawParseRechargeConfig() if err != nil { setupLog.Error(err, "parse recharge config failed") @@ -194,11 +187,6 @@ func main() { DiscountRates: discountRatios, DiscountSteps: discountSteps, } - billingInfoQueryReconciler.Activities = activities - billingInfoQueryReconciler.DefaultDiscount = types.RechargeDiscount{ - DiscountRates: discountRatios, - DiscountSteps: discountSteps, - } } setupManagerError := func(err error, controller string) { setupLog.Error(err, "unable to create controller", "controller", controller) @@ -230,13 +218,6 @@ func main() { setupLog.Error(err, "unable to get property type") os.Exit(1) } - - if err = (&controllers.BillingRecordQueryReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr, rateOpts); err != nil { - setupManagerError(err, "BillingRecordQuery") - } if err = (&controllers.BillingReconciler{ DBClient: dbClient, Properties: resources.DefaultPropertyTypeLS, @@ -259,16 +240,6 @@ func main() { }).SetupWithManager(mgr); err != nil { setupManagerError(err, "Namespace") } - if err = (&controllers.NamespaceBillingHistoryReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupManagerError(err, "NamespaceBillingHistory") - } - billingInfoQueryReconciler.AccountSystemNamespace = accountReconciler.AccountSystemNamespace - if err = (billingInfoQueryReconciler).SetupWithManager(mgr); err != nil { - setupManagerError(err, "BillingInfoQuery") - } if err = (&controllers.PaymentReconciler{ Account: accountReconciler, diff --git a/controllers/devbox/.gitignore b/controllers/devbox/.gitignore index ada68ff086c..fa6acefb0a0 100644 --- a/controllers/devbox/.gitignore +++ b/controllers/devbox/.gitignore @@ -25,3 +25,6 @@ go.work *.swp *.swo *~ + +# ignore deploy.yaml +deploy/manifests/deploy.yaml diff --git a/controllers/devbox/api/v1alpha1/devbox_types.go b/controllers/devbox/api/v1alpha1/devbox_types.go index facdbe59f9b..5c82533e142 100644 --- a/controllers/devbox/api/v1alpha1/devbox_types.go +++ b/controllers/devbox/api/v1alpha1/devbox_types.go @@ -163,16 +163,16 @@ const ( DevboxPhasePending DevboxPhase = "Pending" //DevboxPhaseStopped means Devbox is stop and stopped success DevboxPhaseStopped DevboxPhase = "Stopped" - //DevboxPhaseStopping means Devbox is stop and not stopped success + //DevboxPhaseStopping means Devbox is stopping DevboxPhaseStopping DevboxPhase = "Stopping" //DevboxPhaseError means Devbox is error DevboxPhaseError DevboxPhase = "Error" + //DevboxPhaseUnknown means Devbox is unknown + DevboxPhaseUnknown DevboxPhase = "Unknown" ) // DevboxStatus defines the observed state of Devbox type DevboxStatus struct { - // +kubebuilder:validation:Optional - DevboxPodPhase corev1.PodPhase `json:"podPhase"` // +kubebuilder:validation:Optional Network NetworkStatus `json:"network"` // +kubebuilder:validation:Optional diff --git a/controllers/devbox/api/v1alpha1/runtime_types.go b/controllers/devbox/api/v1alpha1/runtime_types.go index cf1e6bf8045..52791dbf79b 100644 --- a/controllers/devbox/api/v1alpha1/runtime_types.go +++ b/controllers/devbox/api/v1alpha1/runtime_types.go @@ -72,12 +72,19 @@ type Component struct { Version string `json:"version"` } +type RuntimeState string + +const ( + RuntimeStateActive RuntimeState = "active" + RuntimeStateDeprecated RuntimeState = "deprecated" +) + // RuntimeSpec defines the desired state of Runtime type RuntimeSpec struct { - // +kubebuilder:validation:Required - Version string `json:"version"` // +kubebuilder:validation:Required ClassRef string `json:"classRef"` + // +kubebuilder:validation:Required + Version string `json:"version"` // +kubebuilder:validation:Optional Components []Component `json:"components,omitempty"` @@ -88,6 +95,13 @@ type RuntimeSpec struct { // +kubebuilder:validation:Required Config Config `json:"config"` + + // +kubebuilder:validation:Optional + RuntimeVersion string `json:"runtimeVersion,omitempty"` + // +kubebuilder:validation:Optional + // +kubebuilder:validation:Enum=active;deprecated + // +kubebuilder:default=active + State RuntimeState `json:"state,omitempty"` } // RuntimeStatus defines the observed state of Runtime @@ -98,6 +112,10 @@ type RuntimeStatus struct { // +kubebuilder:object:root=true // +kubebuilder:subresource:status +// +kubebuilder:printcolumn:name="Class",type=string,JSONPath=`.spec.classRef` +// +kubebuilder:printcolumn:name="Version",type=string,JSONPath=`.spec.version` +// +kubebuilder:printcolumn:name="RuntimeVersion",type=string,JSONPath=`.spec.runtimeVersion` +// +kubebuilder:printcolumn:name="State",type=string,JSONPath=`.spec.state` // Runtime is the Schema for the runtimes API type Runtime struct { diff --git a/controllers/devbox/cmd/main.go b/controllers/devbox/cmd/main.go index 8dd3f3d7f0d..fba7e1dd261 100644 --- a/controllers/devbox/cmd/main.go +++ b/controllers/devbox/cmd/main.go @@ -24,11 +24,16 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. _ "k8s.io/client-go/plugin/pkg/client/auth" + "k8s.io/client-go/rest" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/cache" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/healthz" "sigs.k8s.io/controller-runtime/pkg/log/zap" "sigs.k8s.io/controller-runtime/pkg/metrics/filters" @@ -64,7 +69,8 @@ func main() { var registryUser string var registryPassword string var authAddr string - var ephemeralStorage string + var requestEphemeralStorage string + var limitEphemeralStorage string var debugMode bool flag.StringVar(®istryAddr, "registry-addr", "sealos.hub:5000", "The address of the registry") flag.StringVar(®istryUser, "registry-user", "admin", "The user of the registry") @@ -81,7 +87,8 @@ func main() { flag.BoolVar(&enableHTTP2, "enable-http2", false, "If set, HTTP/2 will be enabled for the metrics and webhook servers") flag.BoolVar(&debugMode, "debug", false, "If set, debug mode will be enabled") - flag.StringVar(&ephemeralStorage, "ephemeral-storage", "2000Mi", "The maximum value of equatorial storage in devbox.") + flag.StringVar(&requestEphemeralStorage, "request-ephemeral-storage", "500Mi", "The request value of ephemeral storage in devbox.") + flag.StringVar(&limitEphemeralStorage, "limit-ephemeral-storage", "10Gi", "The limit value of ephemeral storage in devbox.") opts := zap.Options{ Development: true, } @@ -133,6 +140,11 @@ func main() { metricsServerOptions.FilterProvider = filters.WithAuthenticationAndAuthorization } + cacheObjLabelSelector := labels.SelectorFromSet(map[string]string{ + "app.kubernetes.io/managed-by": "sealos", + "app.kubernetes.io/part-of": "devbox", + }) + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsServerOptions, @@ -151,6 +163,15 @@ func main() { // if you are doing or is intended to do any operation such as perform cleanups // after the manager stops then its usage might be unsafe. // LeaderElectionReleaseOnCancel: true, + + NewCache: func(config *rest.Config, opts cache.Options) (cache.Cache, error) { + opts.ByObject = map[client.Object]cache.ByObject{ + &corev1.Service{}: {Label: cacheObjLabelSelector}, + &corev1.Pod{}: {Label: cacheObjLabelSelector}, + &corev1.Secret{}: {Label: cacheObjLabelSelector}, + } + return cache.New(config, opts) + }, }) if err != nil { setupLog.Error(err, "unable to start manager") @@ -158,12 +179,13 @@ func main() { } if err = (&controller.DevboxReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - CommitImageRegistry: registryAddr, - Recorder: mgr.GetEventRecorderFor("devbox-controller"), - EquatorialStorage: ephemeralStorage, - DebugMode: debugMode, + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + CommitImageRegistry: registryAddr, + Recorder: mgr.GetEventRecorderFor("devbox-controller"), + RequestEphemeralStorage: requestEphemeralStorage, + LimitEphemeralStorage: limitEphemeralStorage, + DebugMode: debugMode, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Devbox") os.Exit(1) diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml index 7c585d60f50..eaf11b9e577 100644 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml +++ b/controllers/devbox/config/crd/bases/devbox.sealos.io_devboxes.yaml @@ -2848,10 +2848,6 @@ spec: type: object phase: type: string - podPhase: - description: PodPhase is a label for the condition of a pod at the - current time. - type: string state: description: |- ContainerState holds a possible state of container. diff --git a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml b/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml index bfaaa6eccbf..00ed31895c3 100644 --- a/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml +++ b/controllers/devbox/config/crd/bases/devbox.sealos.io_runtimes.yaml @@ -28,7 +28,20 @@ spec: singular: runtime scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.classRef + name: Class + type: string + - jsonPath: .spec.version + name: Version + type: string + - jsonPath: .spec.runtimeVersion + name: RuntimeVersion + type: string + - jsonPath: .spec.state + name: State + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Runtime is the Schema for the runtimes API @@ -2002,6 +2015,14 @@ spec: type: object description: type: string + runtimeVersion: + type: string + state: + default: active + enum: + - active + - deprecated + type: string version: type: string required: diff --git a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl index 753ca75c462..7286e29b5b8 100644 --- a/controllers/devbox/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/devbox/deploy/manifests/deploy.yaml.tmpl @@ -2856,10 +2856,6 @@ spec: type: object phase: type: string - podPhase: - description: PodPhase is a label for the condition of a pod at the - current time. - type: string state: description: |- ContainerState holds a possible state of container. @@ -3133,7 +3129,20 @@ spec: singular: runtime scope: Namespaced versions: - - name: v1alpha1 + - additionalPrinterColumns: + - jsonPath: .spec.classRef + name: Class + type: string + - jsonPath: .spec.version + name: Version + type: string + - jsonPath: .spec.runtimeVersion + name: RuntimeVersion + type: string + - jsonPath: .spec.state + name: State + type: string + name: v1alpha1 schema: openAPIV3Schema: description: Runtime is the Schema for the runtimes API @@ -5107,6 +5116,14 @@ spec: type: object description: type: string + runtimeVersion: + type: string + state: + default: active + enum: + - active + - deprecated + type: string version: type: string required: @@ -5639,7 +5656,7 @@ metadata: name: devbox-controller-manager namespace: devbox-system spec: - replicas: 1 + replicas: 2 selector: matchLabels: control-plane: controller-manager diff --git a/controllers/devbox/internal/controller/devbox_controller.go b/controllers/devbox/internal/controller/devbox_controller.go index 530a059c9ae..0de5776e765 100644 --- a/controllers/devbox/internal/controller/devbox_controller.go +++ b/controllers/devbox/internal/controller/devbox_controller.go @@ -44,8 +44,9 @@ import ( // DevboxReconciler reconciles a Devbox object type DevboxReconciler struct { - CommitImageRegistry string - EquatorialStorage string + CommitImageRegistry string + RequestEphemeralStorage string + LimitEphemeralStorage string DebugMode bool @@ -169,6 +170,15 @@ func (r *DevboxReconciler) syncSecret(ctx context.Context, devbox *devboxv1alpha err := r.Get(ctx, client.ObjectKey{Namespace: devbox.Namespace, Name: devbox.Name}, devboxSecret) if err == nil { // Secret already exists, no need to create + + // TODO: delete this code after we have a way to sync secret to devbox + // check if SEALOS_DEVBOX_JWT_SECRET is exist, if not exist, create it + if _, ok := devboxSecret.Data["SEALOS_DEVBOX_JWT_SECRET"]; !ok { + devboxSecret.Data["SEALOS_DEVBOX_JWT_SECRET"] = []byte(rand.String(32)) + if err := r.Update(ctx, devboxSecret); err != nil { + return fmt.Errorf("failed to update secret: %w", err) + } + } return nil } if client.IgnoreNotFound(err) != nil { @@ -185,6 +195,7 @@ func (r *DevboxReconciler) syncSecret(ctx context.Context, devbox *devboxv1alpha ObjectMeta: objectMeta, Data: map[string][]byte{ "SEALOS_DEVBOX_PASSWORD": []byte(rand.String(12)), + "SEALOS_DEVBOX_JWT_SECRET": []byte(rand.String(32)), "SEALOS_DEVBOX_PUBLIC_KEY": publicKey, "SEALOS_DEVBOX_PRIVATE_KEY": privateKey, }, @@ -202,6 +213,17 @@ func (r *DevboxReconciler) syncSecret(ctx context.Context, devbox *devboxv1alpha func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, recLabels map[string]string) error { logger := log.FromContext(ctx) + + var podList corev1.PodList + if err := r.List(ctx, &podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil { + return err + } + // only one pod is allowed, if more than one pod found, return error + if len(podList.Items) > 1 { + return fmt.Errorf("more than one pod found") + } + logger.Info("pod list", "length", len(podList.Items)) + // update devbox status after pod is created or updated defer func() { if err := retry.RetryOnConflict(retry.DefaultRetry, func() error { @@ -214,6 +236,7 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D // update devbox status with latestDevbox status logger.Info("updating devbox status") logger.Info("merge commit history", "devbox", devbox.Status.CommitHistory, "latestDevbox", latestDevbox.Status.CommitHistory) + devbox.Status.Phase = helper.GenerateDevboxPhase(devbox, podList) helper.UpdateDevboxStatus(devbox, latestDevbox) return r.Status().Update(ctx, latestDevbox) }); err != nil { @@ -225,17 +248,6 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D r.Recorder.Eventf(devbox, corev1.EventTypeNormal, "Sync pod success", "Sync pod success") }() - var podList corev1.PodList - if err := r.List(ctx, &podList, client.InNamespace(devbox.Namespace), client.MatchingLabels(recLabels)); err != nil { - return err - } - // only one pod is allowed, if more than one pod found, return error - if len(podList.Items) > 1 { - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError - return fmt.Errorf("more than one pod found") - } - logger.Info("pod list", "length", len(podList.Items)) - switch devbox.Spec.State { case devboxv1alpha1.DevboxStateRunning: runtimecr, err := r.getRuntime(ctx, devbox) @@ -249,14 +261,23 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D case 0: logger.Info("create pod") logger.Info("next commit history", "commit", nextCommitHistory) - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending - return r.createPod(ctx, devbox, expectPod, nextCommitHistory) + err := r.createPod(ctx, devbox, expectPod, nextCommitHistory) + if err != nil && helper.IsExceededQuotaError(err) { + logger.Info("devbox is exceeded quota, change devbox state to Stopped") + r.Recorder.Eventf(devbox, corev1.EventTypeWarning, "Devbox is exceeded quota", "Devbox is exceeded quota") + devbox.Spec.State = devboxv1alpha1.DevboxStateStopped + _ = r.Update(ctx, devbox) + return nil + } + if err != nil { + logger.Error(err, "create pod failed") + return err + } + return nil case 1: pod := &podList.Items[0] - devbox.Status.DevboxPodPhase = pod.Status.Phase // check pod container size, if it is 0, it means the pod is not running, return an error if len(pod.Status.ContainerStatuses) == 0 { - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending return fmt.Errorf("pod container size is 0") } devbox.Status.State = pod.Status.ContainerStatuses[0].State @@ -275,32 +296,26 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D case corev1.PodPending, corev1.PodRunning: // pod is running or pending, do nothing here logger.Info("pod is running or pending") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseRunning // update commit history status by pod status helper.UpdateCommitHistory(devbox, pod, false) return nil case corev1.PodFailed, corev1.PodSucceeded: // pod failed or succeeded, we need delete pod and remove finalizer - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped logger.Info("pod failed or succeeded, recreate pod") return r.deletePod(ctx, devbox, pod) } case false: // pod not match expectations, delete pod anyway logger.Info("pod not match expectations, recreate pod") - devbox.Status.Phase = devboxv1alpha1.DevboxPhasePending return r.deletePod(ctx, devbox, pod) } } case devboxv1alpha1.DevboxStateStopped: switch len(podList.Items) { case 0: - // update devbox status to stopped, no pod found, do nothing - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped return nil case 1: pod := &podList.Items[0] - devbox.Status.DevboxPodPhase = pod.Status.Phase // update state to empty since devbox is stopped devbox.Status.State = corev1.ContainerState{} // update commit predicated status by pod status, this should be done once find a pod @@ -309,7 +324,6 @@ func (r *DevboxReconciler) syncPod(ctx context.Context, devbox *devboxv1alpha1.D if !pod.DeletionTimestamp.IsZero() { return r.handlePodDeleted(ctx, devbox, pod) } - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseStopped // we need delete pod because devbox state is stopped // we don't care about the pod status, just delete it return r.deletePod(ctx, devbox, pod) @@ -417,12 +431,9 @@ func (r *DevboxReconciler) getRuntime(ctx context.Context, devbox *devboxv1alpha // create a new pod, add predicated status to nextCommitHistory func (r *DevboxReconciler) createPod(ctx context.Context, devbox *devboxv1alpha1.Devbox, expectPod *corev1.Pod, nextCommitHistory *devboxv1alpha1.CommitHistory) error { - logger := log.FromContext(ctx) nextCommitHistory.Status = devboxv1alpha1.CommitStatusPending nextCommitHistory.PredicatedStatus = devboxv1alpha1.CommitStatusPending if err := r.Create(ctx, expectPod); err != nil { - logger.Error(err, "create pod failed") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError return err } devbox.Status.CommitHistory = append(devbox.Status.CommitHistory, nextCommitHistory) @@ -439,7 +450,6 @@ func (r *DevboxReconciler) deletePod(ctx context.Context, devbox *devboxv1alpha1 } if err := r.Delete(ctx, pod); err != nil { logger.Error(err, "delete pod failed") - devbox.Status.Phase = devboxv1alpha1.DevboxPhaseError return err } // update commit history status because pod has been deleted @@ -503,7 +513,8 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt // set up ports and env by using runtime ports and devbox extra ports ports := runtime.Spec.Config.Ports - ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...) + // TODO: add extra ports to pod, currently not support + // ports = append(ports, devbox.Spec.NetworkSpec.ExtraPorts...) envs := runtime.Spec.Config.Env envs = append(envs, devbox.Spec.ExtraEnvs...) @@ -536,7 +547,7 @@ func (r *DevboxReconciler) generateDevboxPod(devbox *devboxv1alpha1.Devbox, runt WorkingDir: helper.GenerateWorkingDir(devbox, runtime), Command: helper.GenerateCommand(devbox, runtime), Args: helper.GenerateDevboxArgs(devbox, runtime), - Resources: helper.GenerateResourceRequirements(devbox, r.EquatorialStorage), + Resources: helper.GenerateResourceRequirements(devbox, r.RequestEphemeralStorage, r.LimitEphemeralStorage), }, } diff --git a/controllers/devbox/internal/controller/helper/devbox.go b/controllers/devbox/internal/controller/helper/devbox.go index 055763ac1cf..eb969bc56f0 100644 --- a/controllers/devbox/internal/controller/helper/devbox.go +++ b/controllers/devbox/internal/controller/helper/devbox.go @@ -18,6 +18,7 @@ import ( "fmt" "log/slog" "sort" + "strings" "crypto/ed25519" "crypto/rand" @@ -76,6 +77,32 @@ func GeneratePodAnnotations(devbox *devboxv1alpha1.Devbox, runtime *devboxv1alph return annotations } +func GenerateDevboxPhase(devbox *devboxv1alpha1.Devbox, podList corev1.PodList) devboxv1alpha1.DevboxPhase { + if len(podList.Items) > 1 { + return devboxv1alpha1.DevboxPhaseError + } + switch devbox.Spec.State { + case devboxv1alpha1.DevboxStateRunning: + if len(podList.Items) == 0 { + return devboxv1alpha1.DevboxPhasePending + } + switch podList.Items[0].Status.Phase { + case corev1.PodFailed, corev1.PodSucceeded: + return devboxv1alpha1.DevboxPhaseStopped + case corev1.PodPending: + return devboxv1alpha1.DevboxPhasePending + case corev1.PodRunning: + return devboxv1alpha1.DevboxPhaseRunning + } + case devboxv1alpha1.DevboxStateStopped: + if len(podList.Items) == 0 { + return devboxv1alpha1.DevboxPhaseStopped + } + return devboxv1alpha1.DevboxPhaseStopping + } + return devboxv1alpha1.DevboxPhaseUnknown +} + func MergeCommitHistory(devbox *devboxv1alpha1.Devbox, latestDevbox *devboxv1alpha1.Devbox) []*devboxv1alpha1.CommitHistory { res := make([]*devboxv1alpha1.CommitHistory, 0) historyMap := make(map[string]*devboxv1alpha1.CommitHistory) @@ -127,7 +154,6 @@ func UpdatePredicatedCommitStatus(devbox *devboxv1alpha1.Devbox, pod *corev1.Pod // TODO: move this function to devbox types.go func UpdateDevboxStatus(current, latest *devboxv1alpha1.Devbox) { latest.Status.Phase = current.Status.Phase - latest.Status.DevboxPodPhase = current.Status.DevboxPodPhase latest.Status.State = current.Status.State latest.Status.LastTerminationState = current.Status.LastTerminationState latest.Status.CommitHistory = MergeCommitHistory(current, latest) @@ -193,6 +219,16 @@ func PodMatchExpectations(expectPod *corev1.Pod, pod *corev1.Pod) bool { return false } + // Check Ephemeral Storage changes + if container.Resources.Requests.StorageEphemeral().Cmp(*expectContainer.Resources.Requests.StorageEphemeral()) != 0 { + slog.Info("Ephemeral-Storage requests are not equal") + return false + } + if container.Resources.Limits.StorageEphemeral().Cmp(*expectContainer.Resources.Limits.StorageEphemeral()) != 0 { + slog.Info("Ephemeral-Storage limits are not equal") + return false + } + // Check environment variables if len(container.Env) != len(expectContainer.Env) { return false @@ -345,22 +381,27 @@ func GenerateSSHVolume(devbox *devboxv1alpha1.Devbox) corev1.Volume { } } -func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, equatorialStorage string) corev1.ResourceRequirements { +func GenerateResourceRequirements(devbox *devboxv1alpha1.Devbox, requestEphemeralStorage, limitEphemeralStorage string) corev1.ResourceRequirements { return corev1.ResourceRequirements{ Requests: calculateResourceRequest( corev1.ResourceList{ - corev1.ResourceCPU: devbox.Spec.Resource["cpu"], - corev1.ResourceMemory: devbox.Spec.Resource["memory"], + corev1.ResourceCPU: devbox.Spec.Resource["cpu"], + corev1.ResourceMemory: devbox.Spec.Resource["memory"], + corev1.ResourceEphemeralStorage: resource.MustParse(requestEphemeralStorage), }, ), Limits: corev1.ResourceList{ - "cpu": devbox.Spec.Resource["cpu"], - "memory": devbox.Spec.Resource["memory"], - "ephemeral-storage": resource.MustParse(equatorialStorage), + corev1.ResourceCPU: devbox.Spec.Resource["cpu"], + corev1.ResourceMemory: devbox.Spec.Resource["memory"], + corev1.ResourceEphemeralStorage: resource.MustParse(limitEphemeralStorage), }, } } +func IsExceededQuotaError(err error) bool { + return strings.Contains(err.Error(), "exceeded quota") +} + func calculateResourceRequest(limit corev1.ResourceList) corev1.ResourceList { if limit == nil { return nil @@ -378,6 +419,11 @@ func calculateResourceRequest(limit corev1.ResourceList) corev1.ResourceList { memoryRequest := memoryValue / rate request[corev1.ResourceMemory] = *resource.NewQuantity(int64(memoryRequest), resource.BinarySI) } + + if ephemeralStorage, ok := limit[corev1.ResourceEphemeralStorage]; ok { + request[corev1.ResourceEphemeralStorage] = ephemeralStorage + } + return request } diff --git a/controllers/objectstorage/api/v1/objectstorageuser_types.go b/controllers/objectstorage/api/v1/objectstorageuser_types.go index 4728948d9fd..98d582a71ea 100644 --- a/controllers/objectstorage/api/v1/objectstorageuser_types.go +++ b/controllers/objectstorage/api/v1/objectstorageuser_types.go @@ -22,6 +22,8 @@ import ( // ObjectStorageUserSpec defines the desired state of ObjectStorageUser type ObjectStorageUserSpec struct { + // +kubebuilder:default=0 + SecretKeyVersion int64 `json:"secretKeyVersion,omitempty"` } // ObjectStorageUserStatus defines the observed state of ObjectStorageUser @@ -34,6 +36,8 @@ type ObjectStorageUserStatus struct { SecretKey string `json:"secretKey,omitempty"` Internal string `json:"internal,omitempty"` External string `json:"external,omitempty"` + // +kubebuilder:default=0 + SecretKeyVersion int64 `json:"secretKeyVersion,omitempty"` } //+kubebuilder:object:root=true diff --git a/controllers/objectstorage/config/crd/bases/objectstorage.sealos.io_objectstorageusers.yaml b/controllers/objectstorage/config/crd/bases/objectstorage.sealos.io_objectstorageusers.yaml index d4f1fa1e20e..8766ee1fb2c 100644 --- a/controllers/objectstorage/config/crd/bases/objectstorage.sealos.io_objectstorageusers.yaml +++ b/controllers/objectstorage/config/crd/bases/objectstorage.sealos.io_objectstorageusers.yaml @@ -48,6 +48,11 @@ spec: type: object spec: description: ObjectStorageUserSpec defines the desired state of ObjectStorageUser + properties: + secretKeyVersion: + default: 0 + format: int64 + type: integer type: object status: description: ObjectStorageUserStatus defines the observed state of ObjectStorageUser @@ -66,6 +71,10 @@ spec: type: integer secretKey: type: string + secretKeyVersion: + default: 0 + format: int64 + type: integer size: description: unit is byte format: int64 diff --git a/controllers/objectstorage/controllers/objectstorageuser_controller.go b/controllers/objectstorage/controllers/objectstorageuser_controller.go index adc3ccd0f28..0b85da99b4d 100644 --- a/controllers/objectstorage/controllers/objectstorageuser_controller.go +++ b/controllers/objectstorage/controllers/objectstorageuser_controller.go @@ -20,6 +20,7 @@ import ( "bytes" "context" "fmt" + "strconv" "strings" "time" @@ -148,9 +149,19 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re } quota := resourceQuota.Spec.Hard.Name(ResourceObjectStorageSize, resource.BinarySI) + used := resourceQuota.Status.Used.Name(ResourceObjectStorageSize, resource.BinarySI) updated := r.initObjectStorageUser(user, username, quota.Value()) + pwdUpdated := false + + if user.Spec.SecretKeyVersion > user.Status.SecretKeyVersion { + user.Status.SecretKey = rand.String(16) + user.Status.SecretKeyVersion = user.Spec.SecretKeyVersion + pwdUpdated = true + updated = true + } + accessKey := user.Status.AccessKey secretKey := user.Status.SecretKey @@ -168,6 +179,13 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re } } + if pwdUpdated { + if err := r.OSAdminClient.SetUser(ctx, accessKey, secretKey, madmin.AccountEnabled); err != nil { + r.Logger.Error(err, "failed to set user secret key", "name", accessKey) + } + r.Logger.V(1).Info("[user] password change info", "name", user.Name, "spec secret key version", user.Spec.SecretKeyVersion) + } + secret := &corev1.Secret{} if err := r.Get(ctx, client.ObjectKey{Name: OSKeySecret, Namespace: userNamespace}, secret); err != nil { if !errors.IsNotFound(err) { @@ -201,6 +219,14 @@ func (r *ObjectStorageUserReconciler) Reconcile(ctx context.Context, req ctrl.Re updated = true } + if used.Value() != size { + resourceQuota.Status.Used[ResourceObjectStorageSize] = resource.MustParse(strconv.FormatInt(size, 10)) + if err := r.Status().Update(ctx, resourceQuota); err != nil { + r.Logger.Error(err, "failed to update status", "name", resourceQuota.Name, "namespace", userNamespace) + return ctrl.Result{}, err + } + } + if user.Status.ObjectsCount != objectsCount { user.Status.ObjectsCount = objectsCount updated = true diff --git a/controllers/pkg/database/cockroach/accountv2.go b/controllers/pkg/database/cockroach/accountv2.go index 09001941633..121046d5328 100644 --- a/controllers/pkg/database/cockroach/accountv2.go +++ b/controllers/pkg/database/cockroach/accountv2.go @@ -15,11 +15,11 @@ package cockroach import ( + "encoding/json" "errors" "fmt" "log" "os" - "path/filepath" "time" "gorm.io/gorm/clause" @@ -39,13 +39,12 @@ import ( ) type Cockroach struct { - DB *gorm.DB - Localdb *gorm.DB - LocalRegion *types.Region - ZeroAccount *types.Account - activities types.Activities - //TODO need init - defaultRechargeDiscount types.RechargeDiscount + DB *gorm.DB + Localdb *gorm.DB + LocalRegion *types.Region + ZeroAccount *types.Account + accountConfig *types.AccountConfig + tasks map[uuid.UUID]types.Task } const ( @@ -120,6 +119,174 @@ func (c *Cockroach) GetUser(ops *types.UserQueryOpts) (*types.User, error) { return &user, nil } +func cloneMap(m map[int64]float64) map[int64]float64 { + newMap := make(map[int64]float64, len(m)) + for k, v := range m { + newMap[k] = v + } + return newMap +} + +func (c *Cockroach) GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get user cr: %v", err) + } + ops.UID = user.UID + } + cfg, err := c.GetAccountConfig() + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get account config: %v", err) + } + isFirstRecharge, err := c.IsNullRecharge(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to check is null recharge: %v", err) + } + defaultSteps, firstRechargeSteps := cfg.DefaultDiscountSteps, cloneMap(cfg.FirstRechargeDiscountSteps) + if !isFirstRecharge && firstRechargeSteps != nil { + payments, err := c.getFirstRechargePayments(ops) + if err != nil { + return types.UserRechargeDiscount{}, fmt.Errorf("failed to get first recharge payments: %v", err) + } + if len(payments) == 0 { + firstRechargeSteps = map[int64]float64{} + } else { + for i := range payments { + delete(firstRechargeSteps, payments[i].Amount/BaseUnit) + } + } + } + return types.UserRechargeDiscount{ + DefaultSteps: defaultSteps, + FirstRechargeSteps: firstRechargeSteps, + }, nil +} + +func (c *Cockroach) GetAccountConfig() (types.AccountConfig, error) { + if c.accountConfig == nil { + config := &types.Configs{} + if err := c.DB.Where(&types.Configs{}).First(config).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return types.AccountConfig{}, nil + } + return types.AccountConfig{}, fmt.Errorf("failed to get account config: %v", err) + } + var accountConfig types.AccountConfig + if err := json.Unmarshal([]byte(config.Data), &accountConfig); err != nil { + return types.AccountConfig{}, fmt.Errorf("failed to unmarshal account config: %v", err) + } + c.accountConfig = &accountConfig + } + return *c.accountConfig, nil +} + +func (c *Cockroach) InsertAccountConfig(config *types.AccountConfig) error { + data, err := json.Marshal(config) + if err != nil { + return fmt.Errorf("failed to marshal account config: %v", err) + } + return c.DB.Model(&types.Configs{}).Create(&types.Configs{Type: types.AccountConfigType, Data: string(data)}).Error +} + +func (c *Cockroach) IsNullRecharge(ops *types.UserQueryOpts) (bool, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return false, fmt.Errorf("failed to get user: %v", err) + } + ops.UID = user.UID + } + var count int64 + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}). + Count(&count).Error; err != nil { + return false, fmt.Errorf("failed to get payment count: %v", err) + } + return count == 0, nil +} + +func (c *Cockroach) getFirstRechargePayments(ops *types.UserQueryOpts) ([]types.Payment, error) { + if ops.UID == uuid.Nil { + user, err := c.GetUser(ops) + if err != nil { + return nil, fmt.Errorf("failed to get user: %v", err) + } + ops.UID = user.UID + } + var payments []types.Payment + if err := c.DB.Model(&types.Payment{}).Where(&types.Payment{PaymentRaw: types.PaymentRaw{UserUID: ops.UID}}).Where(`"activityType" = ?`, types.ActivityTypeFirstRecharge). + Find(&payments).Error; err != nil { + return nil, fmt.Errorf("failed to get payment count: %v", err) + } + return payments, nil +} + +func (c *Cockroach) ProcessPendingTaskRewards() error { + userTasks, err := c.getPendingRewardUserTask() + if err != nil { + return fmt.Errorf("failed to get pending reward user task: %w", err) + } + tasks, err := c.getTask() + if err != nil { + return fmt.Errorf("failed to get tasks: %w", err) + } + for i := range userTasks { + err = c.DB.Transaction(func(tx *gorm.DB) error { + task := tasks[userTasks[i].TaskID] + if task.Reward == 0 { + fmt.Printf("usertask %v reward is 0, skip\n", userTasks[i]) + return nil + } + if err = c.updateBalanceRaw(tx, &types.UserQueryOpts{UID: userTasks[i].UserUID}, task.Reward, false, true, true); err != nil { + return fmt.Errorf("failed to update balance: %w", err) + } + msg := fmt.Sprintf("task %s reward", task.Title) + transaction := types.AccountTransaction{ + Balance: task.Reward, + Type: string(task.TaskType) + "_Reward", + UserUID: userTasks[i].UserUID, + ID: uuid.New(), + Message: &msg, + BillingID: userTasks[i].ID, + } + if err = tx.Save(&transaction).Error; err != nil { + return fmt.Errorf("failed to save transaction: %w", err) + } + return c.completeRewardUserTask(tx, &userTasks[i]) + }) + if err != nil { + return fmt.Errorf("failed to process reward pending user task %v rewards: %w", userTasks[i], err) + } + } + return nil +} + +func (c *Cockroach) getTask() (map[uuid.UUID]types.Task, error) { + if len(c.tasks) != 0 { + return c.tasks, nil + } + c.tasks = make(map[uuid.UUID]types.Task) + var tasks []types.Task + if err := c.DB.Model(&types.Task{IsActive: true, IsNewUserTask: true}).Find(&tasks).Error; err != nil { + return nil, fmt.Errorf("failed to get tasks: %v", err) + } + for i := range tasks { + c.tasks[tasks[i].ID] = tasks[i] + } + return c.tasks, nil +} + +func (c *Cockroach) getPendingRewardUserTask() ([]types.UserTask, error) { + var userTasks []types.UserTask + return userTasks, c.DB.Where(&types.UserTask{Status: types.TaskStatusCompleted, RewardStatus: types.TaskStatusNotCompleted}). + Find(&userTasks).Error +} + +func (c *Cockroach) completeRewardUserTask(tx *gorm.DB, userTask *types.UserTask) error { + userTask.RewardStatus = types.TaskStatusCompleted + return tx.Model(userTask).Update("rewardStatus", types.TaskStatusCompleted).Error +} + func (c *Cockroach) GetUserCr(ops *types.UserQueryOpts) (*types.RegionUserCr, error) { if ops.UID == uuid.Nil && ops.Owner == "" { if ops.ID == "" { @@ -314,6 +481,10 @@ func (c *Cockroach) GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.Oaut } func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool) error { + return c.updateBalanceRaw(tx, ops, amount, isDeduction, add, false) +} + +func (c *Cockroach) updateBalanceRaw(tx *gorm.DB, ops *types.UserQueryOpts, amount int64, isDeduction, add bool, isActive bool) error { if ops.UID == uuid.Nil { user, err := c.GetUserCr(ops) if err != nil { @@ -334,6 +505,9 @@ func (c *Cockroach) updateBalance(tx *gorm.DB, ops *types.UserQueryOpts, amount if err := c.updateWithAccount(isDeduction, add, account, amount); err != nil { return err } + if isActive { + account.ActivityBonus = account.ActivityBonus + amount + } if err := tx.Save(account).Error; err != nil { return fmt.Errorf("failed to update account balance: %w", err) } @@ -359,6 +533,12 @@ func (c *Cockroach) AddBalance(ops *types.UserQueryOpts, amount int64) error { }) } +func (c *Cockroach) AddRewardBalance(ops *types.UserQueryOpts, amount int64, db *gorm.DB) error { + return db.Transaction(func(tx *gorm.DB) error { + return c.updateBalance(tx, ops, amount, false, true) + }) +} + func (c *Cockroach) ReduceBalance(ops *types.UserQueryOpts, amount int64) error { return c.DB.Transaction(func(tx *gorm.DB) error { return c.updateBalance(tx, ops, amount, false, false) @@ -409,153 +589,6 @@ func (c *Cockroach) CreateAccount(ops *types.UserQueryOpts, account *types.Accou return account, nil } -func (c *Cockroach) CreateErrorAccountCreate(account *types.Account, owner, errorMsg string) error { - accountErrSave := &types.ErrorAccountCreate{ - Account: *account, - UserCr: owner, - ErrorTime: time.Now().UTC(), - Message: errorMsg, - RegionUserOwner: owner, - RegionUID: c.LocalRegion.UID, - } - if err := c.DB.FirstOrCreate(accountErrSave, types.ErrorAccountCreate{UserCr: owner}).Error; err != nil { - return fmt.Errorf("failed to create error account create error msg: %w", err) - } - return nil -} - -func (c *Cockroach) CreateErrorPaymentCreate(payment types.Payment, errorMsg string) error { - if err := c.DB.Create(&types.ErrorPaymentCreate{ - PaymentRaw: payment.PaymentRaw, Message: errorMsg, CreateTime: time.Now().UTC()}).Error; err != nil { - return fmt.Errorf("failed to create error payment create error msg: %w", err) - } - return nil -} - -// TransferAccountV1 account indicates the CRD value of the original account -func (c *Cockroach) TransferAccountV1(owner string, account *types.Account) (*types.Account, error) { - //transfer := &types.TransferAccountV1{} - //// if existed, it indicates that the system has been migrated - //err := g.DB.Where(&types.TransferAccountV1{RegionUID: g.LocalRegion.UID, RegionUserOwner: owner}).First(transfer).Error - //if err == nil { - // return nil, nil - //} - //if !errors.Is(err, gorm.ErrRecordNotFound) { - // return nil, fmt.Errorf("failed to get transfer account: %w", err) - //} - - if _, err := os.Stat(filepath.Join(transferAccountV1, c.LocalRegion.UID.String(), owner)); err == nil { - return nil, nil - } else if !os.IsNotExist(err) { - return nil, fmt.Errorf("failed to get transfer account: %v", err) - } - - // if not existed, it indicates that the system has not been migrated - - query := &types.UserQueryOpts{Owner: owner, IgnoreEmpty: true} - accountV2, err := c.GetAccount(query) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - if err = c.saveNullUserRecord(types.NullUserRecord{ - CrName: owner, - RegionID: c.LocalRegion.UID.String(), - }); err != nil { - return nil, fmt.Errorf("failed to save null user record: %v", err) - } - //nullUser := &types.NullUserRecord{ - // CrName: owner, - // RegionID: g.LocalRegion.UID.String(), - //} - //if err := g.DB.FirstOrCreate(nullUser, types.NullUserRecord{CrName: owner, RegionID: g.LocalRegion.UID.String()}).Error; err != nil { - // return nil, fmt.Errorf("failed to create null user record: %v", err) - //} - return nil, nil - } - return nil, fmt.Errorf("failed to get account: %v", err) - } - transfer := types.TransferAccountV1{ - RegionUID: c.LocalRegion.UID, - RegionUserOwner: owner, - } - if accountV2 == nil { - accountV2 = &types.Account{ - UserUID: query.UID, - ActivityBonus: account.ActivityBonus, - EncryptDeductionBalance: account.EncryptDeductionBalance, - EncryptBalance: account.EncryptBalance, - Balance: account.Balance, - DeductionBalance: account.DeductionBalance, - CreateRegionID: c.LocalRegion.UID.String(), - //TODO need init - CreatedAt: account.CreatedAt, - } - if err := c.DB.FirstOrCreate(accountV2).Error; err != nil { - return nil, fmt.Errorf("failed to create account: %w", err) - } - } else { - if accountV2.CreatedAt.After(account.CreatedAt) { - accountV2.CreatedAt = account.CreatedAt - } - if err := c.updateWithAccount(true, true, accountV2, account.DeductionBalance); err != nil { - return nil, fmt.Errorf("failed to update account DeductionBalance: %v", err) - } - if err := c.updateWithAccount(false, true, accountV2, account.Balance); err != nil { - return nil, fmt.Errorf("failed to update account Balance: %v", err) - } - if err := c.DB.Save(accountV2).Error; err != nil { - return nil, fmt.Errorf("failed to save account: %v", err) - } - transfer.Exist = true - } - - transfer.Account = *accountV2 - //if err := g.DB.Save(&transfer).Error; err != nil { - // return fmt.Errorf("failed to save transfer account: %v", err) - //} - if err := c.saveTransferAccountV1(transfer); err != nil { - return nil, fmt.Errorf("failed to save transfer account: %v", err) - } - return accountV2, err -} - -var ( - transferV1toV2 = "transferv1tov2" - transferAccountV1 = filepath.Join(transferV1toV2, "transfer_account_v1") - transferV1Exist = filepath.Join(transferV1toV2, "transfer_account_v1_exist") - nullUserRecord = filepath.Join(transferV1toV2, "null_user_record") -) - -func (c *Cockroach) saveTransferAccountV1(transfer types.TransferAccountV1) error { - name := transfer.RegionUserOwner - savePath := filepath.Join(transferAccountV1, transfer.RegionUID.String(), name) - file, err := os.Create(savePath) - if err != nil { - return fmt.Errorf("failed to create file: %v", err) - } - defer file.Close() - if !transfer.Exist { - return nil - } - saveExistPath := filepath.Join(transferV1Exist, transfer.RegionUID.String(), name) - existFile, err := os.Create(saveExistPath) - if err != nil { - return fmt.Errorf("failed to create file: %v", err) - } - return existFile.Close() -} - -func (c *Cockroach) saveNullUserRecord(nullUser types.NullUserRecord) error { - savePath := filepath.Join(nullUserRecord, nullUser.RegionID, nullUser.CrName) - file, err := os.Create(savePath) - if err != nil { - if errors.Is(err, os.ErrExist) { - return nil - } - return fmt.Errorf("failed to create file: %v", err) - } - return file.Close() -} - func (c *Cockroach) Payment(payment *types.Payment) error { return c.payment(payment, true) } @@ -856,56 +889,6 @@ func (c *Cockroach) NewAccount(ops *types.UserQueryOpts) (*types.Account, error) return account, nil } -func (c *Cockroach) GetUserAccountRechargeDiscount(ops *types.UserQueryOpts) (*types.RechargeDiscount, error) { - userID := ops.UID - if userID == uuid.Nil { - user, err := c.GetUserCr(ops) - if err != nil { - return nil, fmt.Errorf("failed to get user %v: %v", ops, err) - } - userID = user.UserUID - } - var userActivities []types.UserActivity - if !c.DB.Migrator().HasTable("UserActivities") { - return &c.defaultRechargeDiscount, nil - } - if err := c.DB.Table("UserActivities").Where(types.UserActivity{ - UserID: userID, - }).Find(userActivities).Error; err != nil { - if err == gorm.ErrRecordNotFound { - return &c.defaultRechargeDiscount, nil - } - return nil, fmt.Errorf("failed to get user activities: %w", err) - } - if len(userActivities) == 0 { - return &c.defaultRechargeDiscount, nil - } - for _, activity := range userActivities { - currentPhase := activity.CurrentPhase - var userPhase types.UserPhase - err := c.DB.Table("UserPhase").Where(types.UserPhase{ - UserActivityID: activity.UserID, - Name: currentPhase, - }).First(&userPhase).Error - if err != nil { - return nil, fmt.Errorf("failed to get user %v phase: %v", ops, err) - } - for _, phase := range c.activities[activity.Name].Phases { - if phase.ID == userPhase.ID { - limitTime, err := time.ParseDuration(phase.RechargeDiscount.LimitDuration) - if err != nil { - return nil, fmt.Errorf("failed to get limitTime %s: %v", phase.RechargeDiscount.LimitDuration, err) - } - if userPhase.RechargeNums >= phase.RechargeDiscount.LimitTimes || userPhase.EndTime.Add(limitTime).After(time.Now()) { - return &c.defaultRechargeDiscount, nil - } - return &phase.RechargeDiscount.RechargeDiscount, nil - } - } - } - return &c.defaultRechargeDiscount, nil -} - const ( BaseUnit = 1_000_000 MinBalance = 10 * BaseUnit @@ -913,8 +896,7 @@ const ( ) var ( - BaseBalance = int64(DefaultBaseBalance) - EncryptBaseBalance string + BaseBalance = int64(DefaultBaseBalance) ) func (c *Cockroach) TransferAccount(from, to *types.UserQueryOpts, amount int64) error { @@ -958,7 +940,7 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64, return fmt.Errorf("insufficient balance in sender account, sender is %v, transfer amount %d, the transferable amount is: %d", sender, amount, sender.Balance-sender.DeductionBalance-MinBalance-sender.ActivityBonus) } } else { - amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance + amount = sender.Balance - sender.DeductionBalance - c.ZeroAccount.Balance - sender.ActivityBonus if amount <= 0 { return ErrInsufficientBalance } @@ -987,7 +969,24 @@ func (c *Cockroach) transferAccount(from, to *types.UserQueryOpts, amount int64, } func (c *Cockroach) InitTables() error { - return CreateTableIfNotExist(c.DB, types.Account{}, types.ErrorAccountCreate{}, types.ErrorPaymentCreate{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{}) + err := CreateTableIfNotExist(c.DB, types.Account{}, types.Payment{}, types.Transfer{}, types.Region{}, types.Invoice{}, types.InvoicePayment{}, types.Configs{}) + if err != nil { + return fmt.Errorf("failed to create table: %v", err) + } + + // TODO: remove this after migration + if !c.DB.Migrator().HasColumn(&types.Payment{}, `activityType`) { + //if err := c.DB.Migrator().AddColumn(&types.Payment{PaymentRaw: types.PaymentRaw{}}, `PaymentRaw."activityType"`); err != nil { + // return fmt.Errorf("failed to add column activityType: %v", err) + //} + fmt.Println("add column activityType") + tableName := types.Payment{}.TableName() + err := c.DB.Exec(`ALTER TABLE "?" ADD COLUMN "activityType" TEXT;`, gorm.Expr(tableName)).Error + if err != nil { + return fmt.Errorf("failed to add column activityType: %v", err) + } + } + return nil } func NewCockRoach(globalURI, localURI string) (*Cockroach, error) { diff --git a/controllers/pkg/database/interface.go b/controllers/pkg/database/interface.go index 642269f532f..53bd72d7284 100644 --- a/controllers/pkg/database/interface.go +++ b/controllers/pkg/database/interface.go @@ -28,7 +28,6 @@ import ( "github.com/labring/sealos/controllers/pkg/common" - accountv1 "github.com/labring/sealos/controllers/account/api/v1" "github.com/labring/sealos/controllers/pkg/resources" ) @@ -45,16 +44,12 @@ type CVM interface { } type Account interface { - //InitDB() 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 SaveObjTraffic(obs ...*types.ObjectStorageTraffic) error GetAllLatestObjTraffic(startTime, endTime time.Time) ([]types.ObjectStorageTraffic, error) HandlerTimeObjBucketSentTraffic(startTime, endTime time.Time, bucket string) (int64, error) GetTimeObjBucketBucket(startTime, endTime time.Time) ([]string, error) - QueryBillingRecords(billingRecordQuery *accountv1.BillingRecordQuery, owner string) error GetUnsettingBillingHandler(owner string) ([]resources.BillingHandler, error) UpdateBillingStatus(orderID string, status resources.BillingStatus) error GetUpdateTimeForCategoryAndPropertyFromMetering(category string, property string) (time.Time, error) @@ -62,7 +57,6 @@ type Account interface { InitDefaultPropertyTypeLS() error SavePropertyTypes(types []resources.PropertyType) error GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error) - //GetNodePortAmount(owner string, endTime time.Time) (int64, 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 GetDistinctMonitorCombinations(startTime, endTime time.Time) ([]resources.Monitor, error) @@ -95,11 +89,13 @@ type AccountV2 interface { GetUserCr(user *types.UserQueryOpts) (*types.RegionUserCr, error) GetUser(ops *types.UserQueryOpts) (*types.User, error) GetAccount(user *types.UserQueryOpts) (*types.Account, error) + GetAccountConfig() (types.AccountConfig, error) + InsertAccountConfig(config *types.AccountConfig) error GetRegions() ([]types.Region, error) GetLocalRegion() types.Region GetUserOauthProvider(ops *types.UserQueryOpts) ([]types.OauthProvider, error) GetWorkspace(namespaces ...string) ([]types.Workspace, error) - GetUserAccountRechargeDiscount(user *types.UserQueryOpts) (*types.RechargeDiscount, error) + GetUserRechargeDiscount(ops *types.UserQueryOpts) (types.UserRechargeDiscount, error) SetAccountCreateLocalRegion(account *types.Account, region string) error CreateUser(oAuth *types.OauthProvider, regionUserCr *types.RegionUserCr, user *types.User, workspace *types.Workspace, userWorkspace *types.UserWorkspace) error AddBalance(user *types.UserQueryOpts, balance int64) error @@ -109,12 +105,9 @@ type AccountV2 interface { Payment(payment *types.Payment) error SavePayment(payment *types.Payment) error GetUnInvoicedPaymentListWithIds(ids []string) ([]types.Payment, error) - CreateErrorPaymentCreate(payment types.Payment, errorMsg string) error CreateAccount(ops *types.UserQueryOpts, account *types.Account) (*types.Account, error) - CreateErrorAccountCreate(account *types.Account, owner, errorMsg string) error TransferAccount(from, to *types.UserQueryOpts, amount int64) error TransferAccountAll(from, to *types.UserQueryOpts) error - TransferAccountV1(owner string, account *types.Account) (*types.Account, error) AddDeductionBalance(user *types.UserQueryOpts, balance int64) error AddDeductionBalanceWithFunc(ops *types.UserQueryOpts, amount int64, preDo, postDo func() error) error } @@ -126,27 +119,12 @@ type Creator interface { CreateTTLTrafficTimeSeries() error } -type MeteringOwnerTimeResult struct { - //Owner string `bson:"owner"` - Time time.Time `bson:"time"` - Amount int64 `bson:"amount"` - Costs map[string]int64 `bson:"costs"` -} - -//func NewDBInterface(ctx context.Context, mongoURI string) (Interface, error) { -// return mongo.NewMongoInterface(ctx, mongoURI) -//} - const ( MongoURI = "MONGO_URI" CVMMongoURI = "CVM_MONGO_URI" GlobalCockroachURI = "GLOBAL_COCKROACH_URI" LocalCockroachURI = "LOCAL_COCKROACH_URI" TrafficMongoURI = "TRAFFIC_MONGO_URI" - //MongoUsername = "MONGO_USERNAME" - //MongoPassword = "MONGO_PASSWORD" - //RetentionDay = "RETENTION_DAY" - //PermanentRetention = "PERMANENT_RETENTION" ) var _ = AccountV2(&cockroach.Cockroach{}) diff --git a/controllers/pkg/database/mongo/account.go b/controllers/pkg/database/mongo/account.go index 53c8adb3ac9..e8dc8ce38ef 100644 --- a/controllers/pkg/database/mongo/account.go +++ b/controllers/pkg/database/mongo/account.go @@ -31,7 +31,6 @@ import ( gonanoid "github.com/matoous/go-nanoid/v2" - accountv1 "github.com/labring/sealos/controllers/account/api/v1" "github.com/labring/sealos/controllers/pkg/resources" "github.com/labring/sealos/controllers/pkg/utils/logger" @@ -83,11 +82,40 @@ type mongoDB struct { TrafficConn string } +type BillingRecordQueryItem struct { + Time metav1.Time `json:"time" bson:"time"` + BillingRecordQueryItemInline `json:",inline"` +} + +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 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 + Payment *PaymentForQuery `json:"payment,omitempty" bson:"payment,omitempty"` +} + +type Costs map[string]int64 + +type PaymentForQuery struct { + Amount int64 `json:"amount,omitempty" bson:"amount,omitempty"` +} + +const ( + // Consumption 消费 + Consumption common.Type = iota +) + type AccountBalanceSpecBSON struct { // Time metav1.Time `json:"time" bson:"time"` // If the Time field is of the time. time type, it cannot be converted to json crd, so use metav1.Time. However, metav1.Time cannot be inserted into mongo, so you need to convert it to time.Time - Time time.Time `json:"time" bson:"time"` - accountv1.BillingRecordQueryItemInline `json:",inline" bson:",inline"` + Time time.Time `json:"time" bson:"time"` + BillingRecordQueryItemInline `json:",inline" bson:",inline"` } func (m *mongoDB) Disconnect(ctx context.Context) error { @@ -165,82 +193,6 @@ func (m *mongoDB) UpdateBillingStatus(orderID string, status resources.BillingSt return nil } -func (m *mongoDB) GetBillingHistoryNamespaces(startTime, endTime *time.Time, billType int, owner string) ([]string, error) { - filter := bson.M{ - "owner": owner, - } - if startTime != nil && endTime != nil { - filter["time"] = bson.M{ - "$gte": startTime.UTC(), - "$lte": endTime.UTC(), - } - } - if billType != -1 { - filter["type"] = billType - } - - pipeline := mongo.Pipeline{ - {{Key: "$match", Value: filter}}, - {{Key: "$group", Value: bson.D{{Key: "_id", Value: nil}, {Key: "namespaces", Value: bson.D{{Key: "$addToSet", Value: "$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) 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{ - {{Key: "$match", Value: filter}}, - {{Key: "$group", Value: bson.D{{Key: "_id", Value: nil}, {Key: "namespaces", Value: bson.D{{Key: "$addToSet", Value: "$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) SaveBillings(billing ...*resources.Billing) error { billings := make([]interface{}, len(billing)) for i, b := range billing { @@ -469,15 +421,6 @@ func (m *mongoDB) SavePropertyTypes(types []resources.PropertyType) error { return err } -/* - monitors = append(monitors, &common.Monitor{ - Category: namespace.Name, - Used: getResourceUsed(podResource), - Time: timeStamp, - Type: resourceMap[name].Type(), - Name: resourceMap[name].Name(), - }) -*/ func (m *mongoDB) GenerateBillingData(startTime, endTime time.Time, prols *resources.PropertyTypeLS, namespaces []string, owner string) (orderID []string, amount int64, err error) { minutes := endTime.Sub(startTime).Minutes() @@ -631,7 +574,7 @@ func (m *mongoDB) GenerateBillingData(startTime, endTime time.Time, prols *resou } billing := resources.Billing{ OrderID: id, - Type: accountv1.Consumption, + Type: Consumption, Namespace: ns, AppType: uint8(appType), AppName: appName, @@ -678,284 +621,6 @@ func (m *mongoDB) GetUpdateTimeForCategoryAndPropertyFromMetering(category strin return result.Time, nil } -func (m *mongoDB) queryBillingRecordsByOrderID(billingRecordQuery *accountv1.BillingRecordQuery, owner string) error { - if billingRecordQuery.Spec.OrderID == "" { - return fmt.Errorf("order id is empty") - } - billingColl := m.getBillingCollection() - matchStage := bson.D{ - primitive.E{Key: "$match", Value: bson.D{ - primitive.E{Key: "order_id", Value: billingRecordQuery.Spec.OrderID}, - primitive.E{Key: "owner", Value: owner}, - }}, - } - var billingRecords []accountv1.BillingRecordQueryItem - ctx := context.Background() - - cursor, err := billingColl.Aggregate(ctx, bson.A{matchStage}) - if err != nil { - return fmt.Errorf("failed to execute aggregate query: %w", err) - } - defer cursor.Close(ctx) - - for cursor.Next(ctx) { - var bsonRecord resources.Billing - if err := cursor.Decode(&bsonRecord); err != nil { - return fmt.Errorf("failed to decode billing record: %w", err) - } - var billingRecord = accountv1.BillingRecordQueryItem{ - Time: metav1.NewTime(bsonRecord.Time), - BillingRecordQueryItemInline: accountv1.BillingRecordQueryItemInline{ - OrderID: bsonRecord.OrderID, - Type: bsonRecord.Type, - Amount: bsonRecord.Amount, - Namespace: bsonRecord.Namespace, - }, - } - switch bsonRecord.Type { - case accountv1.Recharge: - paymentAmount := billingRecord.Amount - if bsonRecord.Payment != nil { - paymentAmount = bsonRecord.Payment.Amount - } - billingRecord.Payment = &accountv1.PaymentForQuery{Amount: paymentAmount} - billingRecords = append(billingRecords, billingRecord) - case accountv1.TransferOut, accountv1.TransferIn: - billingRecords = append(billingRecords, billingRecord) - default: - for _, cost := range bsonRecord.AppCosts { - billingRecord = accountv1.BillingRecordQueryItem{ - Time: metav1.NewTime(bsonRecord.Time), - BillingRecordQueryItemInline: accountv1.BillingRecordQueryItemInline{ - OrderID: bsonRecord.OrderID, - Type: bsonRecord.Type, - Namespace: bsonRecord.Namespace, - AppType: resources.AppTypeReverse[bsonRecord.AppType], - Costs: resources.ConvertEnumUsedToString(cost.UsedAmount), - Amount: cost.Amount, - Name: cost.Name, - }, - } - billingRecords = append(billingRecords, billingRecord) - } - } - } - - billingRecordQuery.Status.Items = billingRecords - billingRecordQuery.Status.PageLength = 1 - billingRecordQuery.Status.TotalCount = len(billingRecords) - return nil -} - -func (m *mongoDB) QueryBillingRecords(billingRecordQuery *accountv1.BillingRecordQuery, owner string) (err error) { - if billingRecordQuery.Spec.OrderID != "" { - return m.queryBillingRecordsByOrderID(billingRecordQuery, owner) - } - if owner == "" { - return fmt.Errorf("owner is empty") - } - - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - billingColl := m.getBillingCollection() - timeMatchValue := bson.D{primitive.E{Key: "$gte", Value: billingRecordQuery.Spec.StartTime.Time}, primitive.E{Key: "$lte", Value: billingRecordQuery.Spec.EndTime.Time}} - matchValue := bson.D{ - primitive.E{Key: "time", Value: timeMatchValue}, - primitive.E{Key: "owner", Value: owner}, - } - if billingRecordQuery.Spec.Type != -1 { - matchValue = append(matchValue, primitive.E{Key: "type", Value: billingRecordQuery.Spec.Type}) - } - if billingRecordQuery.Spec.Namespace != "" { - matchValue = append(matchValue, primitive.E{Key: "namespace", Value: billingRecordQuery.Spec.Namespace}) - } - if billingRecordQuery.Spec.AppType != "" { - matchValue = append(matchValue, primitive.E{Key: "app_type", Value: resources.AppType[strings.ToUpper(billingRecordQuery.Spec.AppType)]}) - } - matchStage := bson.D{ - primitive.E{ - Key: "$match", Value: matchValue, - }, - } - - // Pipeline for getting the paginated data - pipeline := bson.A{ - matchStage, - bson.D{primitive.E{Key: "$sort", Value: bson.D{primitive.E{Key: "time", Value: -1}}}}, - bson.D{primitive.E{Key: "$skip", Value: (billingRecordQuery.Spec.Page - 1) * billingRecordQuery.Spec.PageSize}}, - bson.D{primitive.E{Key: "$limit", Value: billingRecordQuery.Spec.PageSize}}, - } - - pipelineAll := bson.A{ - matchStage, - bson.D{primitive.E{Key: "$group", Value: bson.D{ - primitive.E{Key: "_id", Value: nil}, - primitive.E{Key: "result", Value: bson.D{primitive.E{Key: "$sum", Value: 1}}}, - }}}, - } - - pipelineCountAndAmount := bson.A{ - matchStage, - bson.D{{Key: "$group", Value: bson.D{ - primitive.E{Key: "_id", Value: nil}, - primitive.E{Key: "result", Value: bson.D{primitive.E{Key: "$sum", Value: "$amount"}}}, - }}}, - } - - pipelineRechargeAmount := bson.A{ - bson.D{{Key: "$match", Value: bson.D{ - {Key: "time", Value: timeMatchValue}, - {Key: "owner", Value: owner}, - {Key: "type", Value: accountv1.Recharge}, - }}}, - bson.D{{Key: "$group", Value: bson.D{ - {Key: "_id", Value: nil}, - {Key: "totalRechargeAmount", Value: bson.D{{Key: "$sum", Value: "$amount"}}}, - {Key: "count", Value: bson.D{{Key: "$sum", Value: 1}}}, - }}}, - } - - cursor, err := billingColl.Aggregate(ctx, pipeline) - if err != nil { - return fmt.Errorf("failed to execute aggregate query: %w", err) - } - defer cursor.Close(ctx) - - var billingRecords []accountv1.BillingRecordQueryItem - for cursor.Next(ctx) { - var bsonRecord resources.Billing - if err := cursor.Decode(&bsonRecord); err != nil { - return fmt.Errorf("failed to decode billing record: %w", err) - } - billingRecord := accountv1.BillingRecordQueryItem{ - Time: metav1.NewTime(bsonRecord.Time), - BillingRecordQueryItemInline: accountv1.BillingRecordQueryItemInline{ - OrderID: bsonRecord.OrderID, - Namespace: bsonRecord.Namespace, - Type: bsonRecord.Type, - AppType: resources.AppTypeReverse[bsonRecord.AppType], - Amount: bsonRecord.Amount, - }, - } - if len(bsonRecord.AppCosts) != 0 { - costs := make(map[string]int64) - for i := range bsonRecord.AppCosts { - for j := range bsonRecord.AppCosts[i].UsedAmount { - costs[resources.DefaultPropertyTypeLS.EnumMap[j].Name] += bsonRecord.AppCosts[i].UsedAmount[j] - } - } - billingRecord.Costs = costs - } - if bsonRecord.Type == accountv1.Recharge { - paymentAmount := billingRecord.Amount - if bsonRecord.Payment != nil { - paymentAmount = bsonRecord.Payment.Amount - } - billingRecord.Payment = &accountv1.PaymentForQuery{Amount: paymentAmount} - } - billingRecords = append(billingRecords, billingRecord) - } - - totalCount := 0 - - // total quantity - cursorAll, err := billingColl.Aggregate(ctx, pipelineAll) - if err != nil { - return fmt.Errorf("failed to execute aggregate all query: %w", err) - } - defer cursorAll.Close(ctx) - for cursorAll.Next(ctx) { - var result struct { - Result int64 `bson:"result"` - } - if err := cursorAll.Decode(&result); err != nil { - return fmt.Errorf("failed to decode query count record: %w", err) - } - totalCount = int(result.Result) - } - - // Costs Executing the second pipeline for getting the total count, recharge and deduction amount - cursorCountAndAmount, err := billingColl.Aggregate(ctx, pipelineCountAndAmount) - if err != nil { - return fmt.Errorf("failed to execute aggregate query for count and amount: %w", err) - } - defer cursorCountAndAmount.Close(ctx) - - totalDeductionAmount, totalRechargeAmount := int64(0), int64(0) - - for cursorCountAndAmount.Next(ctx) { - var result struct { - Result int64 `bson:"result"` - } - if err := cursorCountAndAmount.Decode(&result); err != nil { - return fmt.Errorf("failed to decode billing record: %w", err) - } - totalDeductionAmount = result.Result - } - - // the total amount - cursorRechargeAmount, err := billingColl.Aggregate(ctx, pipelineRechargeAmount) - if err != nil { - return fmt.Errorf("failed to execute aggregate query for recharge amount: %w", err) - } - defer cursorRechargeAmount.Close(ctx) - - for cursorRechargeAmount.Next(ctx) { - var result struct { - TotalRechargeAmount int64 `bson:"totalRechargeAmount"` - Count int `bson:"count"` - } - if err := cursorRechargeAmount.Decode(&result); err != nil { - return fmt.Errorf("failed to decode recharge amount record: %w", err) - } - totalRechargeAmount = result.TotalRechargeAmount - } - - totalPages := (totalCount + billingRecordQuery.Spec.PageSize - 1) / billingRecordQuery.Spec.PageSize - if totalCount == 0 { - totalPages = 1 - totalCount = len(billingRecords) - } - billingRecordQuery.Status.Items, billingRecordQuery.Status.PageLength, billingRecordQuery.Status.TotalCount, - billingRecordQuery.Status.RechargeAmount, billingRecordQuery.Status.DeductionAmount = billingRecords, totalPages, totalCount, totalRechargeAmount, totalDeductionAmount - return nil -} - -//func (m *mongoDB) GetNodePortAmount(owner string, endTime time.Time) (int64, error) { -// filter := bson.M{ -// "owner": owner, -// "time": bson.M{ -// "$lte": endTime, -// }, -// "type": accountv1.Consumption, -// "used_amount.4": bson.M{"$ne": 0}, -// } -// -// cursor, err := m.getBillingCollection().Find(context.Background(), filter) -// if err != nil { -// return 0, fmt.Errorf("failed to execute aggregate query: %w", err) -// } -// defer cursor.Close(context.Background()) -// -// var billings []resources.Billing -// if err := cursor.All(context.Background(), &billings); err != nil { -// return 0, fmt.Errorf("failed to decode all billing record: %w", err) -// } -// amountTotal := int64(0) -// for i := range billings { -// for j := range billings[i].AppCosts { -// amount := billings[i].AppCosts[j].UsedAmount[4] -// if amount > 0 { -// amountTotal += amount -// } -// } -// } -// -// return amountTotal, nil -// -//} - func (m *mongoDB) GetBillingCount(accountType common.Type, startTime, endTime time.Time) (count, amount int64, err error) { pipeline := bson.A{ bson.M{ diff --git a/controllers/pkg/database/mongo/account_test.go b/controllers/pkg/database/mongo/account_test.go index f8480747a61..82f3f94f121 100644 --- a/controllers/pkg/database/mongo/account_test.go +++ b/controllers/pkg/database/mongo/account_test.go @@ -26,222 +26,11 @@ import ( "github.com/labring/sealos/controllers/pkg/resources" - "github.com/dustin/go-humanize" - "sigs.k8s.io/yaml" - "go.mongodb.org/mongo-driver/mongo" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - - accountv1 "github.com/labring/sealos/controllers/account/api/v1" ) -func TestMongoDB_QueryBillingRecords(t *testing.T) { - dbCTX := context.Background() - - m, err := NewMongoInterface(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) - } - }() - - testTime := time.Date(2021, 1, 1, 0, 0, 0, 0, time.Local) - startTime, endTime := metav1.Time{Time: testTime}, metav1.Time{Time: testTime.Add(3 * humanize.Day)} - // page 1 pageSize 1 type -1 - query1 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: startTime, - EndTime: endTime, - Page: 1, - PageSize: 1, - //OrderID: "random_order_id_1", - OrderID: "", - Type: -1, - }, - } - // page 1 pageSize 5 type 1 - query2 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: startTime, - EndTime: endTime, - Page: 1, - PageSize: 5, - //OrderID: "random_order_id_1", - OrderID: "", - Type: 1, - }, - } - // page 1 pageSize 5 type 0 - query3 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: startTime, - EndTime: endTime, - Page: 1, - PageSize: 5, - //OrderID: "random_order_id_1", - OrderID: "", - Type: 0, - }, - } - // only orderID - - query4 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - OrderID: "random_order_id_recharge19", - }, - } - - query5 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: startTime, - EndTime: endTime, - Page: 2, - PageSize: 5, - Namespace: "ns-vd1k1dk3", - Type: 1, - }, - } - - billingRecordQueryList := []*accountv1.BillingRecordQuery{ - query1, query2, query3, query4, query5, - } - for _, billingRecordQuery := range billingRecordQueryList { - err = m.QueryBillingRecords(billingRecordQuery, "vd1k1dk3") - if err != nil { - t.Errorf("failed to query billing records: error = %v", err) - } - data, err := yaml.Marshal(billingRecordQuery) - if err != nil { - t.Errorf("failed to marshal billingRecordQuery: error = %v", err) - } - t.Logf("billingRecordQuery: %s\n", string(data)) - } -} - -func TestMongoDB_QueryBillingRecords1(t *testing.T) { - dbCTX := context.Background() - m, err := NewMongoInterface(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) - } - }() - //now := time.Now().UTC() - billquery := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: metav1.Time{Time: time.Now().UTC().Add(-time.Hour * 24 * 30)}, - EndTime: metav1.Time{Time: time.Now().UTC().Add(time.Hour * 24 * 30)}, - Page: 1, - PageSize: 5, - Type: 1, - }, - } - err = m.QueryBillingRecords(billquery, "") - if err != nil { - t.Errorf("failed to query billing records: error = %v", err) - } - data, err := yaml.Marshal(billquery) - if err != nil { - t.Errorf("failed to marshal billingRecordQuery: error = %v", err) - } - t.Logf("billingRecordQuery: %s\n", string(data)) -} - var testTime = time.Date(2023, 5, 9, 5, 0, 0, 0, time.UTC) -func TestMongoDB_QueryBillingRecords2(t *testing.T) { - dbCTX := context.Background() - - os.Setenv("MONGODB_URI", "mongodb://root:lv4nfcgz@127.0.0.1:64110/sealos-resources?authSource=admin&directConnection=true") - m, err := NewMongoInterface(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) - } - }() - now := time.Now().UTC() - testTime := time.Date(now.Year(), now.Month(), now.Day(), now.Hour(), 0, 0, 0, time.Local).UTC() - startTime, endTime := metav1.Time{Time: testTime}, metav1.Time{Time: testTime.Add(3 * humanize.Day)} - // page 1 pageSize 1 type -1 - query1 := &accountv1.BillingRecordQuery{ - Spec: accountv1.BillingRecordQuerySpec{ - StartTime: startTime, - EndTime: endTime, - Page: 1, - PageSize: 15, - //OrderID: "random_order_id_1", - OrderID: "", - Type: -1, - }, - } - //// page 1 pageSize 5 type 1 - //query2 := &accountv1.BillingRecordQuery{ - // Spec: accountv1.BillingRecordQuerySpec{ - // StartTime: startTime, - // EndTime: endTime, - // Page: 1, - // PageSize: 5, - // //OrderID: "random_order_id_1", - // OrderID: "", - // Type: 1, - // }, - //} - //// page 1 pageSize 5 type 0 - //query3 := &accountv1.BillingRecordQuery{ - // Spec: accountv1.BillingRecordQuerySpec{ - // StartTime: startTime, - // EndTime: endTime, - // Page: 1, - // PageSize: 5, - // //OrderID: "random_order_id_1", - // OrderID: "", - // Type: 0, - // }, - //} - //// only orderID - // - //query4 := &accountv1.BillingRecordQuery{ - // Spec: accountv1.BillingRecordQuerySpec{ - // OrderID: "random_order_id_recharge19", - // }, - //} - // - //query5 := &accountv1.BillingRecordQuery{ - // Spec: accountv1.BillingRecordQuerySpec{ - // StartTime: startTime, - // EndTime: endTime, - // Page: 2, - // PageSize: 5, - // Namespace: "ns-vd1k1dk3", - // Type: 1, - // }, - //} - - billingRecordQueryList := []*accountv1.BillingRecordQuery{ - query1, /*query2, query3, query4, query5,*/ - } - for _, billingRecordQuery := range billingRecordQueryList { - err = m.QueryBillingRecords(billingRecordQuery, "1jc12uh6") - if err != nil { - t.Errorf("failed to query billing records: error = %v", err) - } - data, err := yaml.Marshal(billingRecordQuery) - if err != nil { - t.Errorf("failed to marshal billingRecordQuery: error = %v", err) - } - t.Logf("billingRecordQuery: %s\n", string(data)) - } -} - func TestMongoDB_SaveBillingsWithAccountBalance(t *testing.T) { type fields struct { URL string @@ -498,30 +287,6 @@ func TestMongoDB_DropMonitorCollectionsOlderThan(t *testing.T) { } } -func TestMongoDB_GetBillingHistoryNamespaceList(t *testing.T) { - dbCTX := context.Background() - m, err := NewMongoInterface(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) -} - /* info generate billing data used {2 ns-7uyfrr47 pay-xy map[0:325 1:166 2:0]} diff --git a/controllers/pkg/database/mongo/user.go b/controllers/pkg/database/mongo/user.go deleted file mode 100644 index 1473771fca2..00000000000 --- a/controllers/pkg/database/mongo/user.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 mongo diff --git a/controllers/pkg/database/mongo/user_test.go b/controllers/pkg/database/mongo/user_test.go deleted file mode 100644 index 1473771fca2..00000000000 --- a/controllers/pkg/database/mongo/user_test.go +++ /dev/null @@ -1,15 +0,0 @@ -// 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 mongo diff --git a/controllers/pkg/metering/README.md b/controllers/pkg/metering/README.md deleted file mode 100644 index dfddf436164..00000000000 --- a/controllers/pkg/metering/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# metering -// TODO(user): Add simple overview of use/purpose - -## Description -// TODO(user): An in-depth paragraph about your project and overview of use - -## Getting Started -You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. -**Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). - -### Running on the cluster -1. Install Instances of Custom Resources: - -```sh -kubectl apply -f config/samples/ -``` - -2. Build and push your image to the location specified by `IMG`: - -```sh -make docker-build docker-push IMG=/metering:tag -``` - -3. Deploy the controller to the cluster with the image specified by `IMG`: - -```sh -make deploy IMG=/metering:tag -``` - -### Uninstall CRDs -To delete the CRDs from the cluster: - -```sh -make uninstall -``` - -### Undeploy controller -UnDeploy the controller from the cluster: - -```sh -make undeploy -``` - -## Contributing -// TODO(user): Add detailed information on how you would like others to contribute to this project - -### How it works -This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/). - -It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/), -which provide a reconcile function responsible for synchronizing resources until the desired state is reached on the cluster. - -### Test It Out -1. Install the CRDs into the cluster: - -```sh -make install -``` - -2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): - -```sh -make run -``` - -**NOTE:** You can also run this in one step by running: `make install run` - -### Modifying the API definitions -If you are editing the API definitions, generate the manifests such as CRs or CRDs using: - -```sh -make manifests -``` - -**NOTE:** Run `make --help` for more information on all potential `make` targets - -More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) - -## License - -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. - diff --git a/controllers/pkg/metering/api/v1/extensionresourceprice_types.go b/controllers/pkg/metering/api/v1/extensionresourceprice_types.go deleted file mode 100644 index 18ad8802fec..00000000000 --- a/controllers/pkg/metering/api/v1/extensionresourceprice_types.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright 2022. - -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 ( - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const ExtensionResourcePricePrefix = "extensionresourceprice-" - -// ExtensionResourcePriceSpec defines the desired state of ExtensionResourcePrice -type ExtensionResourcePriceSpec struct { - ResourceName string `json:"resourceName,omitempty"` - Resources map[v1.ResourceName]ResourcePrice `json:"resources,omitempty"` - GroupVersionKinds []GroupVersionKind `json:"groupVersionKinds"` -} - -type GroupVersionKind struct { - Group string `json:"group,omitempty"` - Version string `json:"version,omitempty"` - Kind string `json:"kind,omitempty"` -} - -type ResourcePrice struct { - Unit *resource.Quantity `json:"unit"` - Price int64 `json:"price"` // 100 = 1¥ - Describe string `json:"describe,omitempty"` -} - -// ExtensionResourcePriceStatus defines the observed state of ExtensionResourcePrice -type ExtensionResourcePriceStatus struct { -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// ExtensionResourcePrice is the Schema for the extensionresourceprices API -type ExtensionResourcePrice struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ExtensionResourcePriceSpec `json:"spec,omitempty"` - Status ExtensionResourcePriceStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ExtensionResourcePriceList contains a list of ExtensionResourcePrice -type ExtensionResourcePriceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []ExtensionResourcePrice `json:"items"` -} - -func init() { - SchemeBuilder.Register(&ExtensionResourcePrice{}, &ExtensionResourcePriceList{}) -} - -func GetExtensionResourcePriceName(resourceControllerName string) string { - return ExtensionResourcePricePrefix + resourceControllerName -} diff --git a/controllers/pkg/metering/api/v1/groupversion_info.go b/controllers/pkg/metering/api/v1/groupversion_info.go deleted file mode 100644 index 4aec0ee1259..00000000000 --- a/controllers/pkg/metering/api/v1/groupversion_info.go +++ /dev/null @@ -1,36 +0,0 @@ -/* -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 contains API Schema definitions for the metering v1 API group -// +kubebuilder:object:generate=true -// +groupName=metering.common.sealos.io -package v1 - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/scheme" -) - -var ( - // GroupVersion is group version used to register these objects - GroupVersion = schema.GroupVersion{Group: "metering.common.sealos.io", Version: "v1"} - - // SchemeBuilder is used to add go types to the GroupVersionKind scheme - SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} - - // AddToScheme adds the types in this group-version to the given scheme. - AddToScheme = SchemeBuilder.AddToScheme -) diff --git a/controllers/pkg/metering/api/v1/resource_types.go b/controllers/pkg/metering/api/v1/resource_types.go deleted file mode 100644 index 0c8fc60746d..00000000000 --- a/controllers/pkg/metering/api/v1/resource_types.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright 2022. - -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 ( - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -type Status string - -const ( - Complete Status = "complete" - Create Status = "create" -) - -// ResourceSpec defines the desired state of Resource -type ResourceSpec struct { - Resources map[v1.ResourceName]ResourceInfo `json:"resources,omitempty"` -} - -type ResourceInfo struct { - ResourceName string `json:"resourceName,omitempty"` - Used *resource.Quantity `json:"used,omitempty"` - Timestamp int64 `json:"time,omitempty"` - Namespace string `json:"namespace,omitempty"` - Cost int64 `json:"cost,omitempty"` -} - -type ResourceInfoList []ResourceInfo - -type ResourcePriceAndUsed struct { - ResourcePrice `json:",inline"` - Used *resource.Quantity `json:"used,omitempty"` -} - -// ResourceStatus defines the observed state of Resource -type ResourceStatus struct { - Status Status `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status - -// Resource is the Schema for the resources API -type Resource struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec ResourceSpec `json:"spec,omitempty"` - Status ResourceStatus `json:"status,omitempty"` -} - -//+kubebuilder:object:root=true - -// ResourceList contains a list of Resource -type ResourceList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []Resource `json:"items"` -} - -func init() { - SchemeBuilder.Register(&Resource{}, &ResourceList{}) -} diff --git a/controllers/pkg/metering/api/v1/zz_generated.deepcopy.go b/controllers/pkg/metering/api/v1/zz_generated.deepcopy.go deleted file mode 100644 index 6f28bfd4486..00000000000 --- a/controllers/pkg/metering/api/v1/zz_generated.deepcopy.go +++ /dev/null @@ -1,285 +0,0 @@ -//go:build !ignore_autogenerated -// +build !ignore_autogenerated - -// Code generated by controller-gen. DO NOT EDIT. - -package v1 - -import ( - corev1 "k8s.io/api/core/v1" - 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 *ExtensionResourcePrice) DeepCopyInto(out *ExtensionResourcePrice) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionResourcePrice. -func (in *ExtensionResourcePrice) DeepCopy() *ExtensionResourcePrice { - if in == nil { - return nil - } - out := new(ExtensionResourcePrice) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ExtensionResourcePrice) 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 *ExtensionResourcePriceList) DeepCopyInto(out *ExtensionResourcePriceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]ExtensionResourcePrice, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionResourcePriceList. -func (in *ExtensionResourcePriceList) DeepCopy() *ExtensionResourcePriceList { - if in == nil { - return nil - } - out := new(ExtensionResourcePriceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ExtensionResourcePriceList) 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 *ExtensionResourcePriceSpec) DeepCopyInto(out *ExtensionResourcePriceSpec) { - *out = *in - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make(map[corev1.ResourceName]ResourcePrice, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionResourcePriceSpec. -func (in *ExtensionResourcePriceSpec) DeepCopy() *ExtensionResourcePriceSpec { - if in == nil { - return nil - } - out := new(ExtensionResourcePriceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ExtensionResourcePriceStatus) DeepCopyInto(out *ExtensionResourcePriceStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ExtensionResourcePriceStatus. -func (in *ExtensionResourcePriceStatus) DeepCopy() *ExtensionResourcePriceStatus { - if in == nil { - return nil - } - out := new(ExtensionResourcePriceStatus) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Resource) DeepCopyInto(out *Resource) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - in.Spec.DeepCopyInto(&out.Spec) - out.Status = in.Status -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Resource. -func (in *Resource) DeepCopy() *Resource { - if in == nil { - return nil - } - out := new(Resource) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *Resource) 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 *ResourceInfo) DeepCopyInto(out *ResourceInfo) { - *out = *in - if in.Used != nil { - in, out := &in.Used, &out.Used - x := (*in).DeepCopy() - *out = &x - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceInfo. -func (in *ResourceInfo) DeepCopy() *ResourceInfo { - if in == nil { - return nil - } - out := new(ResourceInfo) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in ResourceInfoList) DeepCopyInto(out *ResourceInfoList) { - { - in := &in - *out = make(ResourceInfoList, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceInfoList. -func (in ResourceInfoList) DeepCopy() ResourceInfoList { - if in == nil { - return nil - } - out := new(ResourceInfoList) - in.DeepCopyInto(out) - return *out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourceList) DeepCopyInto(out *ResourceList) { - *out = *in - out.TypeMeta = in.TypeMeta - in.ListMeta.DeepCopyInto(&out.ListMeta) - if in.Items != nil { - in, out := &in.Items, &out.Items - *out = make([]Resource, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceList. -func (in *ResourceList) DeepCopy() *ResourceList { - if in == nil { - return nil - } - out := new(ResourceList) - in.DeepCopyInto(out) - return out -} - -// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. -func (in *ResourceList) 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 *ResourcePrice) DeepCopyInto(out *ResourcePrice) { - *out = *in - if in.Unit != nil { - in, out := &in.Unit, &out.Unit - x := (*in).DeepCopy() - *out = &x - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePrice. -func (in *ResourcePrice) DeepCopy() *ResourcePrice { - if in == nil { - return nil - } - out := new(ResourcePrice) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourcePriceAndUsed) DeepCopyInto(out *ResourcePriceAndUsed) { - *out = *in - in.ResourcePrice.DeepCopyInto(&out.ResourcePrice) - if in.Used != nil { - in, out := &in.Used, &out.Used - x := (*in).DeepCopy() - *out = &x - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourcePriceAndUsed. -func (in *ResourcePriceAndUsed) DeepCopy() *ResourcePriceAndUsed { - if in == nil { - return nil - } - out := new(ResourcePriceAndUsed) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourceSpec) DeepCopyInto(out *ResourceSpec) { - *out = *in - if in.Resources != nil { - in, out := &in.Resources, &out.Resources - *out = make(map[corev1.ResourceName]ResourceInfo, len(*in)) - for key, val := range *in { - (*out)[key] = *val.DeepCopy() - } - } -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceSpec. -func (in *ResourceSpec) DeepCopy() *ResourceSpec { - if in == nil { - return nil - } - out := new(ResourceSpec) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *ResourceStatus) DeepCopyInto(out *ResourceStatus) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ResourceStatus. -func (in *ResourceStatus) DeepCopy() *ResourceStatus { - if in == nil { - return nil - } - out := new(ResourceStatus) - in.DeepCopyInto(out) - return out -} diff --git a/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_extensionresourceprices.yaml b/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_extensionresourceprices.yaml deleted file mode 100644 index 35f69b51d79..00000000000 --- a/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_extensionresourceprices.yaml +++ /dev/null @@ -1,96 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null - name: extensionresourceprices.metering.common.sealos.io -spec: - group: metering.common.sealos.io - names: - kind: ExtensionResourcePrice - listKind: ExtensionResourcePriceList - plural: extensionresourceprices - singular: extensionresourceprice - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: ExtensionResourcePrice is the Schema for the extensionresourceprices - 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: ExtensionResourcePriceSpec defines the desired state of ExtensionResourcePrice - properties: - groupVersionKinds: - items: - properties: - group: - type: string - kind: - type: string - version: - type: string - type: object - type: array - resourceName: - type: string - resources: - additionalProperties: - properties: - describe: - type: string - price: - format: int64 - type: integer - unit: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - required: - - price - - unit - type: object - type: object - required: - - groupVersionKinds - type: object - status: - description: ExtensionResourcePriceStatus defines the observed state of - ExtensionResourcePrice - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_resources.yaml b/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_resources.yaml deleted file mode 100644 index d52f19b0e92..00000000000 --- a/controllers/pkg/metering/config/crd/bases/metering.common.sealos.io_resources.yaml +++ /dev/null @@ -1,84 +0,0 @@ -# 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. - ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.11.3 - creationTimestamp: null - name: resources.metering.common.sealos.io -spec: - group: metering.common.sealos.io - names: - kind: Resource - listKind: ResourceList - plural: resources - singular: resource - scope: Namespaced - versions: - - name: v1 - schema: - openAPIV3Schema: - description: Resource is the Schema for the resources 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: ResourceSpec defines the desired state of Resource - properties: - resources: - additionalProperties: - properties: - cost: - format: int64 - type: integer - namespace: - type: string - resourceName: - type: string - time: - format: int64 - type: integer - used: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - type: object - type: object - status: - description: ResourceStatus defines the observed state of Resource - properties: - status: - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/controllers/pkg/metering/config/crd/kustomization.yaml b/controllers/pkg/metering/config/crd/kustomization.yaml deleted file mode 100644 index 4b6586b42a2..00000000000 --- a/controllers/pkg/metering/config/crd/kustomization.yaml +++ /dev/null @@ -1,38 +0,0 @@ -# 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. - -# This kustomization.yaml is not intended to be run by itself, -# since it depends on service name and namespace that are out of this kustomize package. -# It should be run by config/default -resources: -- bases/metering.common.sealos.io_resources.yaml -- bases/metering.common.sealos.io_extensionresourceprices.yaml -#+kubebuilder:scaffold:crdkustomizeresource - -patchesStrategicMerge: -# [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. -# patches here are for enabling the conversion webhook for each CRD -#- patches/webhook_in_resources.yaml -#- patches/webhook_in_extensionresourceprice.yaml -#+kubebuilder:scaffold:crdkustomizewebhookpatch - -# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. -# patches here are for enabling the CA injection for each CRD -#- patches/cainjection_in_resources.yaml -#- patches/cainjection_in_extensionresourceprice.yaml -#+kubebuilder:scaffold:crdkustomizecainjectionpatch - -# the following config is for teaching kustomize how to do kustomization for CRDs. -configurations: -- kustomizeconfig.yaml diff --git a/controllers/pkg/metering/config/crd/patches/cainjection_in_extensionresourceprice.yaml b/controllers/pkg/metering/config/crd/patches/cainjection_in_extensionresourceprice.yaml deleted file mode 100644 index 8339ef6b7e4..00000000000 --- a/controllers/pkg/metering/config/crd/patches/cainjection_in_extensionresourceprice.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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: extensionresourceprice.metering.common.sealos.io diff --git a/controllers/pkg/metering/config/crd/patches/cainjection_in_resources.yaml b/controllers/pkg/metering/config/crd/patches/cainjection_in_resources.yaml deleted file mode 100644 index 7c078e0bf8f..00000000000 --- a/controllers/pkg/metering/config/crd/patches/cainjection_in_resources.yaml +++ /dev/null @@ -1,21 +0,0 @@ -# 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. - -# 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: resources.metering.common.sealos.io diff --git a/controllers/pkg/resources/named.go b/controllers/pkg/resources/named.go index 5e214a12ccd..cd02083d466 100644 --- a/controllers/pkg/resources/named.go +++ b/controllers/pkg/resources/named.go @@ -21,29 +21,11 @@ import ( corev1 "k8s.io/api/core/v1" - sealos_networkmanager "github.com/dinoallo/sealos-networkmanager-protoapi" - "sigs.k8s.io/controller-runtime/pkg/client" ) -/* -AppType (sort by label) : - Database: app.kubernetes.io/instance=gitea app.kubernetes.io/managed-by=kubeblocks apps.kubeblocks.io/component-name - AppLaunchpad:app: xxx - Terminal: TerminalID: xxx - Cronjob:job-name: xxx - Other: in addition to the above all labels -*/ - -const ( - Pod = "Pod" - PVC = "PVC" -) - const ( DBPodLabelInstanceKey = "app.kubernetes.io/instance" - DBPodLabelManagedByKey = "app.kubernetes.io/managed-by" - DBPodLabelManagedByValue = "kubeblocks" DBPodLabelComponentNameKey = "apps.kubeblocks.io/component-name" TerminalIDLabelKey = "TerminalID" AppLabelKey = "app" @@ -154,93 +136,6 @@ func (r *ResourceNamed) ParentName() string { return r.parentName } -func (r *ResourceNamed) Labels() map[string]string { - label := make(map[string]string) - switch r.Type() { - case db: - label[DBPodLabelComponentNameKey] = r.labels[DBPodLabelComponentNameKey] - label[DBPodLabelInstanceKey] = r.labels[DBPodLabelInstanceKey] - case terminal: - label[TerminalIDLabelKey] = r.labels[TerminalIDLabelKey] - case app: - label[AppLabelKey] = r.labels[AppLabelKey] - case job: - label[JobNameLabelKey] = r.labels[JobNameLabelKey] - //case other: - //default: - } - return label -} - -var notExistLabels = func() map[uint8][]*sealos_networkmanager.Label { - labels := make(map[uint8][]*sealos_networkmanager.Label) - for k := range AppTypeReverse { - labels[k] = getNotExistLabels(k) - } - return labels -}() - -func (r *ResourceNamed) GetNotExistLabels() []*sealos_networkmanager.Label { - return notExistLabels[r.Type()] -} - -func getNotExistLabels(tp uint8) []*sealos_networkmanager.Label { - var labels []*sealos_networkmanager.Label - for appType := range AppTypeReverse { - if tp == appType { - continue - } - switch appType { - case db: - labels = append(labels, &sealos_networkmanager.Label{ - Key: DBPodLabelComponentNameKey, - }, &sealos_networkmanager.Label{ - Key: DBPodLabelManagedByKey, - }) - case app: - labels = append(labels, &sealos_networkmanager.Label{ - Key: AppLabelKey, - }) - case terminal: - labels = append(labels, &sealos_networkmanager.Label{ - Key: TerminalIDLabelKey, - }) - case job: - labels = append(labels, &sealos_networkmanager.Label{ - Key: JobNameLabelKey, - }) - } - } - return labels -} - -func (r *ResourceNamed) GetInLabels() []*sealos_networkmanager.Label { - var labelsEqual []*sealos_networkmanager.Label - switch r.Type() { - case db: - labelsEqual = append(labelsEqual, &sealos_networkmanager.Label{ - Key: DBPodLabelComponentNameKey, - Value: []string{r.labels[DBPodLabelComponentNameKey]}, - }) - case terminal: - labelsEqual = append(labelsEqual, &sealos_networkmanager.Label{ - Key: TerminalIDLabelKey, - Value: []string{r.labels[TerminalIDLabelKey]}, - }) - case app: - labelsEqual = append(labelsEqual, &sealos_networkmanager.Label{ - Key: AppLabelKey, - Value: []string{r.labels[AppLabelKey]}, - }) - case job: - labelsEqual = append(labelsEqual, &sealos_networkmanager.Label{ - Key: JobNameLabelKey, - Value: []string{r.labels[JobNameLabelKey]}, - }) - } - return labelsEqual -} - func (r *ResourceNamed) TypeString() string { return r._type } diff --git a/controllers/pkg/types/activity.go b/controllers/pkg/types/activity.go index ddf78ab5896..50b98410d30 100644 --- a/controllers/pkg/types/activity.go +++ b/controllers/pkg/types/activity.go @@ -23,6 +23,12 @@ import ( "gorm.io/gorm" ) +type UserRechargeDiscount struct { + DefaultSteps map[int64]float64 `json:"defaultSteps,omitempty" bson:"defaultSteps,omitempty"` + FirstRechargeSteps map[int64]float64 `json:"firstRechargeDiscount,omitempty" bson:"firstRechargeDiscount,omitempty"` +} + +// TODO the following structures will be deleted type Activity struct { gorm.Model ActivityType string `gorm:"uniqueIndex"` diff --git a/controllers/pkg/types/config.go b/controllers/pkg/types/config.go new file mode 100644 index 00000000000..a09983014de --- /dev/null +++ b/controllers/pkg/types/config.go @@ -0,0 +1,36 @@ +/* +Copyright 2024. + +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 types + +type Configs struct { + Type ConfigType `json:"type" gorm:"type:varchar(255);not null,primaryKey"` + Data string `json:"data" gorm:"type:jsonb"` +} + +type ConfigType string + +const AccountConfigType ConfigType = "account" + +type AccountConfig struct { + TaskProcessRegion string `json:"taskProcessRegion"` + FirstRechargeDiscountSteps map[int64]float64 `json:"firstRechargeDiscountSteps"` + DefaultDiscountSteps map[int64]float64 `json:"defaultDiscountSteps"` +} + +func (c Configs) TableName() string { + return "Configs" +} diff --git a/controllers/pkg/types/global.go b/controllers/pkg/types/global.go index 38070d4d995..12ce3a8db7a 100644 --- a/controllers/pkg/types/global.go +++ b/controllers/pkg/types/global.go @@ -187,46 +187,6 @@ func (RegionUserCr) TableName() string { return "UserCr" } -type TransferAccountV1 struct { - //RealUser RealUser - RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"` - RegionUserOwner string `gorm:"column:regionUserOwner;type:text;not null"` - Exist bool `gorm:"type:boolean;default:false"` - Account -} - -func (TransferAccountV1) TableName() string { - return "TransferAccountV1" -} - -type NullUserRecord struct { - CrName string `gorm:"column:crName;type:text;not null;unique"` - RegionID string `gorm:"type:text;not null"` -} - -func (NullUserRecord) TableName() string { - return "NullUserRecord" -} - -type ErrorAccountCreate struct { - Account - UserCr string `gorm:"column:userCr;type:text;not null;unique"` - ErrorTime time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"` - RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"` - RegionUserOwner string `gorm:"column:regionUserOwner;type:text;not null"` - Message string `gorm:"type:text;not null"` -} - -func (ErrorAccountCreate) TableName() string { - return "ErrorAccountCreate" -} - -type ErrorPaymentCreate struct { - PaymentRaw - CreateTime time.Time `gorm:"type:timestamp(3) with time zone;default:current_timestamp()"` - Message string `gorm:"type:text;not null"` -} - type PaymentRaw struct { UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null"` RegionUID uuid.UUID `gorm:"column:regionUid;type:uuid;not null"` @@ -237,15 +197,18 @@ type PaymentRaw struct { Gift int64 `gorm:"type:bigint"` TradeNO string `gorm:"type:text;unique;not null"` // CodeURL is the codeURL of wechatpay - CodeURL string `gorm:"type:text"` - InvoicedAt bool `gorm:"type:boolean;default:false"` - Remark string `gorm:"type:text"` - Message string `gorm:"type:text;not null"` + CodeURL string `gorm:"type:text"` + InvoicedAt bool `gorm:"type:boolean;default:false"` + Remark string `gorm:"type:text"` + ActivityType ActivityType `gorm:"type:text;column:activityType"` + Message string `gorm:"type:text;not null"` } -func (ErrorPaymentCreate) TableName() string { - return "ErrorPaymentCreate" -} +type ActivityType string + +const ( + ActivityTypeFirstRecharge ActivityType = "FIRST_RECHARGE" +) type Payment struct { ID string `gorm:"type:text;primary_key"` diff --git a/controllers/pkg/types/task.go b/controllers/pkg/types/task.go new file mode 100644 index 00000000000..3522281dd85 --- /dev/null +++ b/controllers/pkg/types/task.go @@ -0,0 +1,80 @@ +/* +Copyright 2024. + +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 types + +import ( + "time" + + "github.com/google/uuid" +) + +// Task represents the Task model in Go with GORM annotations. +type Task struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + Title string `gorm:"column:title;type:text;not null" json:"title"` + Description string `gorm:"column:description;type:text;not null" json:"description"` + Reward int64 `gorm:"column:reward;type:bigint;not null" json:"reward"` + Order int `gorm:"column:order;type:integer;not null" json:"order"` + IsActive bool `gorm:"column:isActive;type:boolean;default:true;not null" json:"isActive"` + IsNewUserTask bool `gorm:"column:isNewUserTask;type:boolean;default:false;not null" json:"isNewUserTask"` + TaskType TaskType `gorm:"column:taskType;type:TaskType;not null" json:"taskType"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` +} + +// UserTask represents the UserTask model in Go with GORM annotations. +type UserTask struct { + ID uuid.UUID `gorm:"column:id;type:uuid;default:gen_random_uuid();primary_key" json:"id"` + UserUID uuid.UUID `gorm:"column:userUid;type:uuid;not null" json:"userUid"` + TaskID uuid.UUID `gorm:"column:taskId;type:uuid;not null" json:"taskId"` + Status TaskStatus `gorm:"column:status;type:TaskStatus;not null" json:"status"` + RewardStatus TaskStatus `gorm:"column:rewardStatus;type:TaskStatus;not null" json:"rewardStatus"` + CompletedAt time.Time `gorm:"column:completedAt;type:timestamp(3);not null" json:"completedAt"` + CreatedAt time.Time `gorm:"column:createdAt;type:timestamp(3) with time zone;default:current_timestamp();not null" json:"createdAt"` + UpdatedAt time.Time `gorm:"column:updatedAt;type:timestamp(3) with time zone;not null" json:"updatedAt"` + + //User User `gorm:"foreignKey:UserUid;references:UID" json:"user"` + //Task Task `gorm:"foreignKey:TaskId;references:ID" json:"task"` +} + +// TableName specifies the table name for GORM +func (Task) TableName() string { + return "Task" +} + +// TableName specifies the table name for GORM +func (UserTask) TableName() string { + return "UserTask" +} + +// TaskType represents the TaskType enum in Go. +type TaskType string + +//const ( +// TaskTypeLaunchpad TaskType = "LAUNCHPAD" +// TaskTypeCostcenter TaskType = "COSTCENTER" +// TaskTypeDatabase TaskType = "DATABASE" +// TaskTypeDesktop TaskType = "DESKTOP" +//) + +// TaskStatus represents the TaskStatus enum in Go. +type TaskStatus string + +const ( + TaskStatusNotCompleted TaskStatus = "NOT_COMPLETED" + TaskStatusCompleted TaskStatus = "COMPLETED" +) diff --git a/controllers/terminal/api/v1/terminal_types.go b/controllers/terminal/api/v1/terminal_types.go index 49701dd8bcf..97b6dca1006 100644 --- a/controllers/terminal/api/v1/terminal_types.go +++ b/controllers/terminal/api/v1/terminal_types.go @@ -56,6 +56,7 @@ type TerminalSpec struct { type TerminalStatus struct { AvailableReplicas int32 `json:"availableReplicas"` ServiceName string `json:"serviceName"` + SecretHeader string `json:"secretHeader"` Domain string `json:"domain"` } @@ -64,7 +65,6 @@ type TerminalStatus struct { //+kubebuilder:printcolumn:name="User",type=string,JSONPath=".spec.user" //+kubebuilder:printcolumn:name="Keepalived",type=string,JSONPath=".spec.keepalived" //+kubebuilder:printcolumn:name="Domain",type=string,JSONPath=".status.domain" -//+kubebuilder:printcolumn:name="APIServer",priority=1,type=string,JSONPath=".spec.apiServer" //+kubebuilder:printcolumn:name="LastUpdateTime",priority=1,type=string,JSONPath=".metadata.annotations.lastUpdateTime" //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" diff --git a/controllers/terminal/config/crd/bases/terminal.sealos.io_terminals.yaml b/controllers/terminal/config/crd/bases/terminal.sealos.io_terminals.yaml index bb422284113..4ef9bcee596 100644 --- a/controllers/terminal/config/crd/bases/terminal.sealos.io_terminals.yaml +++ b/controllers/terminal/config/crd/bases/terminal.sealos.io_terminals.yaml @@ -38,10 +38,6 @@ spec: - jsonPath: .status.domain name: Domain type: string - - jsonPath: .spec.apiServer - name: APIServer - priority: 1 - type: string - jsonPath: .metadata.annotations.lastUpdateTime name: LastUpdateTime priority: 1 @@ -107,11 +103,14 @@ spec: type: integer domain: type: string + secretHeader: + type: string serviceName: type: string required: - availableReplicas - domain + - secretHeader - serviceName type: object type: object diff --git a/controllers/terminal/controllers/ingress.go b/controllers/terminal/controllers/ingress.go index 5f51091e5be..4ddaa93d563 100644 --- a/controllers/terminal/controllers/ingress.go +++ b/controllers/terminal/controllers/ingress.go @@ -37,6 +37,15 @@ if ($flag = '02'){ return 403; }` func (r *TerminalReconciler) createNginxIngress(terminal *terminalv1.Terminal, host string) *networkingv1.Ingress { cors := fmt.Sprintf("https://%s,https://*.%s", r.CtrConfig.Global.CloudDomain+r.getPort(), r.CtrConfig.Global.CloudDomain+r.getPort()) + secretHeader := terminal.Status.SecretHeader + configurationSnippet := safeConfigurationSnippet + ` +proxy_set_header Authorization ""; +proxy_set_header ` + secretHeader + ` "1";` + + higressReqHeaderUpdate := ` +Authorization "" +` + secretHeader + ` "1"` + objectMeta := metav1.ObjectMeta{ Name: terminal.Name, Namespace: terminal.Namespace, @@ -50,7 +59,8 @@ func (r *TerminalReconciler) createNginxIngress(terminal *terminalv1.Terminal, h "nginx.ingress.kubernetes.io/cors-allow-origin": cors, "nginx.ingress.kubernetes.io/cors-allow-methods": "PUT, GET, POST, PATCH, OPTIONS", "nginx.ingress.kubernetes.io/cors-allow-credentials": "false", - "nginx.ingress.kubernetes.io/configuration-snippet": safeConfigurationSnippet, + "nginx.ingress.kubernetes.io/configuration-snippet": configurationSnippet, + "higress.io/request-header-control-update": higressReqHeaderUpdate, }, } diff --git a/controllers/terminal/controllers/terminal_controller.go b/controllers/terminal/controllers/terminal_controller.go index 355d034924a..8035da62a65 100644 --- a/controllers/terminal/controllers/terminal_controller.go +++ b/controllers/terminal/controllers/terminal_controller.go @@ -18,6 +18,7 @@ package controllers import ( "context" + "strings" "time" "github.com/jaevor/go-nanoid" @@ -32,9 +33,11 @@ import ( "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" "github.com/labring/sealos/controllers/pkg/utils/label" terminalv1 "github.com/labring/sealos/controllers/terminal/api/v1" @@ -65,6 +68,10 @@ const ( MemoryLimit = "256Mi" ) +const ( + SecretHeaderPrefix = "X-SEALOS-" +) + // TerminalReconciler reconciles a Terminal object type TerminalReconciler struct { client.Client @@ -123,6 +130,13 @@ func (r *TerminalReconciler) Reconcile(ctx context.Context, req ctrl.Request) (c } } + if terminal.Status.SecretHeader == "" { + terminal.Status.SecretHeader = r.generateSecretHeader() + if err := r.Status().Update(ctx, terminal); err != nil { + return ctrl.Result{}, err + } + } + recLabels := label.RecommendedLabels(&label.Recommended{ Name: terminal.Name, ManagedBy: label.DefaultManagedBy, @@ -262,6 +276,8 @@ func (r *TerminalReconciler) syncDeployment(ctx context.Context, terminal *termi {Name: "USER_TOKEN", Value: terminal.Spec.Token}, {Name: "NAMESPACE", Value: terminal.Namespace}, {Name: "USER_NAME", Value: terminal.Spec.User}, + // Add secret header + {Name: "AUTH_HEADER", Value: terminal.Status.SecretHeader}, } containers = []corev1.Container{ @@ -377,12 +393,18 @@ func (r *TerminalReconciler) getPort() string { return ":" + r.CtrConfig.Global.CloudPort } +func (r *TerminalReconciler) generateSecretHeader() string { + return SecretHeaderPrefix + strings.ToUpper(rand.String(5)) +} + // SetupWithManager sets up the controller with the Manager. func (r *TerminalReconciler) SetupWithManager(mgr ctrl.Manager) error { r.recorder = mgr.GetEventRecorderFor("sealos-terminal-controller") r.Config = mgr.GetConfig() return ctrl.NewControllerManagedBy(mgr). - For(&terminalv1.Terminal{}). - Owns(&appsv1.Deployment{}).Owns(&corev1.Service{}).Owns(&networkingv1.Ingress{}). + For(&terminalv1.Terminal{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&appsv1.Deployment{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). + Owns(&corev1.Service{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). + Owns(&networkingv1.Ingress{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})). Complete(r) } diff --git a/controllers/terminal/deploy/manifests/deploy.yaml.tmpl b/controllers/terminal/deploy/manifests/deploy.yaml.tmpl index 51e6eb50941..c92a9e8c6c8 100644 --- a/controllers/terminal/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/terminal/deploy/manifests/deploy.yaml.tmpl @@ -31,10 +31,6 @@ spec: - jsonPath: .status.domain name: Domain type: string - - jsonPath: .spec.apiServer - name: APIServer - priority: 1 - type: string - jsonPath: .metadata.annotations.lastUpdateTime name: LastUpdateTime priority: 1 @@ -91,11 +87,14 @@ spec: type: integer domain: type: string + secretHeader: + type: string serviceName: type: string required: - availableReplicas - domain + - secretHeader - serviceName type: object type: object diff --git a/controllers/user/Makefile b/controllers/user/Makefile index 3fada975794..b585fcd4c90 100644 --- a/controllers/user/Makefile +++ b/controllers/user/Makefile @@ -123,8 +123,8 @@ CONTROLLER_GEN ?= $(LOCALBIN)/controller-gen ENVTEST ?= $(LOCALBIN)/setup-envtest ## Tool Versions -KUSTOMIZE_VERSION ?= v3.8.7 -CONTROLLER_TOOLS_VERSION ?= v0.10.0 +KUSTOMIZE_VERSION ?= v5.3.0 +CONTROLLER_TOOLS_VERSION ?= v0.14.0 KUSTOMIZE_INSTALL_SCRIPT ?= "https://raw.githubusercontent.com/kubernetes-sigs/kustomize/master/hack/install_kustomize.sh" .PHONY: kustomize diff --git a/controllers/user/api/v1/deleterequest_types.go b/controllers/user/api/v1/deleterequest_types.go index 387badd52a2..7b4ca3dd1fb 100644 --- a/controllers/user/api/v1/deleterequest_types.go +++ b/controllers/user/api/v1/deleterequest_types.go @@ -36,6 +36,7 @@ type DeleteRequestStatus struct { // +kubebuilder:resource:scope=Cluster //+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user" //+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" // DeleteRequest is the Schema for the deleterequests API type DeleteRequest struct { diff --git a/controllers/user/api/v1/operationrequest_types.go b/controllers/user/api/v1/operationrequest_types.go index 481345252e1..8b0d2adf294 100644 --- a/controllers/user/api/v1/operationrequest_types.go +++ b/controllers/user/api/v1/operationrequest_types.go @@ -22,7 +22,9 @@ import ( // OperationrequestSpec defines the desired state of Operationrequest type OperationrequestSpec struct { - User string `json:"user,omitempty"` + // Namespace is the workspace that needs to be operated. + Namespace string `json:"namespace,omitempty"` + User string `json:"user,omitempty"` // +kubebuilder:validation:Enum=Owner;Manager;Developer Role RoleType `json:"role,omitempty"` // +kubebuilder:validation:Enum=Grant;Update;Deprive @@ -56,9 +58,11 @@ const ( ) //+kubebuilder:printcolumn:name="Action",type="string",JSONPath=".spec.action" +//+kubebuilder:printcolumn:name="Namespace",type="string",JSONPath=".spec.namespace" //+kubebuilder:printcolumn:name="User",type="string",JSONPath=".spec.user" //+kubebuilder:printcolumn:name="Role",type="string",JSONPath=".spec.role" //+kubebuilder:printcolumn:name="Phase",type="string",JSONPath=".status.phase" +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" //+kubebuilder:object:root=true //+kubebuilder:subresource:status diff --git a/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml index a457df17460..575311e7219 100644 --- a/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml +++ b/controllers/user/config/crd/bases/user.sealos.io_deleterequests.yaml @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: deleterequests.user.sealos.io spec: group: user.sealos.io @@ -36,20 +35,28 @@ spec: - jsonPath: .status.phase name: Phase type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1 schema: openAPIV3Schema: description: DeleteRequest is the Schema for the deleterequests 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' + 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' + 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 diff --git a/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml index 96537708dec..cfabdc44c85 100644 --- a/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml +++ b/controllers/user/config/crd/bases/user.sealos.io_operationrequests.yaml @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: operationrequests.user.sealos.io spec: group: user.sealos.io @@ -33,6 +32,9 @@ spec: - jsonPath: .spec.action name: Action type: string + - jsonPath: .spec.namespace + name: Namespace + type: string - jsonPath: .spec.user name: User type: string @@ -42,20 +44,28 @@ spec: - jsonPath: .status.phase name: Phase type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1 schema: openAPIV3Schema: description: Operationrequest is the Schema for the operation requests 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' + 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' + 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 @@ -68,6 +78,9 @@ spec: - Update - Deprive type: string + namespace: + description: Namespace is the workspace that needs to be operated. + type: string role: enum: - Owner diff --git a/controllers/user/config/crd/bases/user.sealos.io_users.yaml b/controllers/user/config/crd/bases/user.sealos.io_users.yaml index 74a0a38c674..b11d6470840 100644 --- a/controllers/user/config/crd/bases/user.sealos.io_users.yaml +++ b/controllers/user/config/crd/bases/user.sealos.io_users.yaml @@ -17,8 +17,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: users.user.sealos.io spec: group: user.sealos.io @@ -45,14 +44,19 @@ spec: description: User is the Schema for the users 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' + 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' + 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 @@ -61,12 +65,14 @@ spec: properties: csrExpirationSeconds: default: 7200 - description: "expirationSeconds is the requested duration of validity - of the issued certificate. The certificate signer may issue a certificate - with a different validity duration so a client must check the delta - between the notBefore and notAfter fields in the issued certificate - to determine the actual duration. \n The minimum valid value for - expirationSeconds is 600, i.e. 10 minutes." + description: |- + expirationSeconds is the requested duration of validity of the issued + certificate. The certificate signer may issue a certificate with a different + validity duration so a client must check the delta between the notBefore and + notAfter fields in the issued certificate to determine the actual duration. + + + The minimum valid value for expirationSeconds is 600, i.e. 10 minutes. format: int32 type: integer type: object diff --git a/controllers/user/config/rbac/role.yaml b/controllers/user/config/rbac/role.yaml index a08d957d42f..4ce169d6ed5 100644 --- a/controllers/user/config/rbac/role.yaml +++ b/controllers/user/config/rbac/role.yaml @@ -16,7 +16,6 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: manager-role rules: - apiGroups: diff --git a/controllers/user/config/webhook/manifests.yaml b/controllers/user/config/webhook/manifests.yaml index 7048ff3da51..f79982aeb70 100644 --- a/controllers/user/config/webhook/manifests.yaml +++ b/controllers/user/config/webhook/manifests.yaml @@ -16,7 +16,6 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: - creationTimestamp: null name: mutating-webhook-configuration webhooks: - admissionReviewVersions: @@ -63,7 +62,6 @@ webhooks: apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: - creationTimestamp: null name: validating-webhook-configuration webhooks: - admissionReviewVersions: diff --git a/controllers/user/controllers/operationrequest_controller.go b/controllers/user/controllers/operationrequest_controller.go index d9bcab550d8..1691fd51436 100644 --- a/controllers/user/controllers/operationrequest_controller.go +++ b/controllers/user/controllers/operationrequest_controller.go @@ -19,8 +19,13 @@ package controllers import ( "context" "fmt" + "sync" "time" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "github.com/go-logr/logr" util "github.com/labring/operator-sdk/controller" @@ -48,6 +53,7 @@ type OperationReqReconciler struct { Logger logr.Logger Scheme *runtime.Scheme Recorder record.EventRecorder + userLock map[string]*sync.Mutex // expirationTime is the time duration of the request is expired expirationTime time.Duration @@ -68,9 +74,10 @@ func (r *OperationReqReconciler) SetupWithManager(mgr ctrl.Manager, opts util.Ra r.Scheme = mgr.GetScheme() r.expirationTime = expTime r.retentionTime = retTime + r.userLock = make(map[string]*sync.Mutex) r.Logger.V(1).Info("init reconcile operationrequest controller") return ctrl.NewControllerManagedBy(mgr). - For(&userv1.Operationrequest{}). + For(&userv1.Operationrequest{}, builder.WithPredicates(namespaceOnlyPredicate(config.GetUserSystemNamespace()))). WithOptions(controller.Options{ MaxConcurrentReconciles: util.GetConcurrent(opts), RateLimiter: util.GetRateLimiter(opts), @@ -78,6 +85,23 @@ func (r *OperationReqReconciler) SetupWithManager(mgr ctrl.Manager, opts util.Ra Complete(r) } +func namespaceOnlyPredicate(namespace string) predicate.Predicate { + return predicate.Funcs{ + CreateFunc: func(e event.CreateEvent) bool { + return e.Object.GetNamespace() == namespace + }, + DeleteFunc: func(e event.DeleteEvent) bool { + return e.Object.GetNamespace() == namespace + }, + UpdateFunc: func(e event.UpdateEvent) bool { + return e.ObjectNew.GetNamespace() == namespace + }, + GenericFunc: func(e event.GenericEvent) bool { + return e.Object.GetNamespace() == namespace + }, + } +} + // +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/status,verbs=get;update;patch // +kubebuilder:rbac:groups=user.sealos.io,resources=operationrequests/finalizers,verbs=update @@ -91,6 +115,13 @@ func (r *OperationReqReconciler) Reconcile(ctx context.Context, req ctrl.Request } func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1.Operationrequest) (ctrl.Result, error) { + userLock, ok := r.userLock[request.Spec.User] + if !ok { + userLock = &sync.Mutex{} + r.userLock[request.Spec.User] = userLock + } + userLock.Lock() + defer userLock.Unlock() r.Logger.V(1).Info("start reconcile controller operationRequest", getLog(request)...) // count the time cost of handling the request startTime := time.Now() @@ -136,17 +167,10 @@ func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1. ) user := &userv1.User{} - if err := r.Get(ctx, client.ObjectKey{Name: config.GetUserNameByNamespace(request.Namespace)}, user); err != nil { + if err := r.Get(ctx, client.ObjectKey{Name: config.GetUserNameByNamespace(request.Spec.Namespace)}, user); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get user", "Failed to get user %s", request.Spec.User) return ctrl.Result{}, err } - if request.Spec.Role == userv1.OwnerRoleType { - if user.Name == user.Annotations[userv1.UserAnnotationOwnerKey] { - // 不允许转移个人空间 - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to grant role", "Failed to grant role %s to user %s, cannot transfer personal workspace", request.Spec.Role, request.Spec.User) - return ctrl.Result{}, r.updateRequestStatus(ctx, request, userv1.RequestFailed) - } - } bindUser := &userv1.User{} if err := r.Get(ctx, client.ObjectKey{Name: request.Spec.User}, bindUser); err != nil { r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to get bind user", "Failed to get bind user %s", request.Spec.User) @@ -184,8 +208,12 @@ func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1. r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to delete rolebinding", "Failed to delete rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) return ctrl.Result{}, err } - if _, err := ctrl.CreateOrUpdate(ctx, r.Client, rolebinding, setUpOwnerReferenceFc); err != nil { - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to create/update rolebinding", "Failed to create rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) + if err := r.Create(ctx, rolebinding); err != nil { + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to create rolebinding", "Failed to create rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) + return ctrl.Result{}, err + } + if err = setUpOwnerReferenceFc(); err != nil { + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to set owner reference", "Failed to set owner reference for rolebinding %s/%s", rolebinding.Namespace, rolebinding.Name) return ctrl.Result{}, err } if request.Spec.Role == userv1.OwnerRoleType { @@ -206,7 +234,7 @@ func (r *OperationReqReconciler) reconcile(ctx context.Context, request *userv1. return ctrl.Result{}, err } - r.Recorder.Eventf(request, v1.EventTypeNormal, "Completed", "Completed operation request %s/%s", request.Namespace, request.Name) + r.Recorder.Eventf(request, v1.EventTypeNormal, "Completed", "Completed operation request %s/%s", request.Spec.Namespace, request.Name) return ctrl.Result{RequeueAfter: OperationReqRequeueDuration}, nil } @@ -234,9 +262,9 @@ func (r *OperationReqReconciler) isExpired(request *userv1.Operationrequest) boo func (r *OperationReqReconciler) deleteRequest(ctx context.Context, request *userv1.Operationrequest) error { r.Logger.V(1).Info("deleting OperationRequest", "request", request) if err := r.Delete(ctx, request); client.IgnoreNotFound(err) != nil { - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to delete OperationRequest", "Failed to delete OperationRequest %s/%s", request.Namespace, request.Name) + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to delete OperationRequest", "Failed to delete OperationRequest %s/%s", request.Spec.Namespace, request.Name) r.Logger.Error(err, "Failed to delete OperationRequest", getLog(request)...) - return fmt.Errorf("failed to delete OperationRequest %s/%s: %w", request.Namespace, request.Name, err) + return fmt.Errorf("failed to delete OperationRequest %s/%s: %w", request.Spec.Namespace, request.Name, err) } r.Logger.V(1).Info("delete OperationRequest success", getLog(request)...) return nil @@ -245,7 +273,7 @@ func (r *OperationReqReconciler) deleteRequest(ctx context.Context, request *use func (r *OperationReqReconciler) updateRequestStatus(ctx context.Context, request *userv1.Operationrequest, phase userv1.RequestPhase) error { request.Status.Phase = phase if err := r.Status().Update(ctx, request); err != nil { - r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to update OperationRequest status", "Failed to update OperationRequest status %s/%s", request.Namespace, request.Name) + r.Recorder.Eventf(request, v1.EventTypeWarning, "Failed to update OperationRequest status", "Failed to update OperationRequest status %s/%s", request.Spec.Namespace, request.Name) r.Logger.V(1).Info("update OperationRequest status failed", getLog(request)...) return err } @@ -257,7 +285,7 @@ func conventRequestToRolebinding(request *userv1.Operationrequest) *rbacv1.RoleB return &rbacv1.RoleBinding{ ObjectMeta: metav1.ObjectMeta{ Name: config.GetGroupRoleBindingName(request.Spec.User), - Namespace: request.Namespace, + Namespace: request.Spec.Namespace, Annotations: map[string]string{ userAnnotationOwnerKey: request.Spec.User, }, @@ -283,7 +311,7 @@ func conventRequestToRolebinding(request *userv1.Operationrequest) *rbacv1.RoleB func getLog(request *userv1.Operationrequest, kv ...interface{}) []interface{} { return append([]interface{}{ "request.name", request.Name, - "request.namespace", request.Namespace, + "request.Spec.Namespace", request.Spec.Namespace, "request.user", request.Spec.User, "request.role", request.Spec.Role, "request.action", request.Spec.Action, diff --git a/controllers/user/deploy/manifests/deploy.yaml.tmpl b/controllers/user/deploy/manifests/deploy.yaml.tmpl index 7ab425ebe02..c9a83881014 100644 --- a/controllers/user/deploy/manifests/deploy.yaml.tmpl +++ b/controllers/user/deploy/manifests/deploy.yaml.tmpl @@ -9,8 +9,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: deleterequests.user.sealos.io spec: group: user.sealos.io @@ -28,20 +27,28 @@ spec: - jsonPath: .status.phase name: Phase type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1 schema: openAPIV3Schema: description: DeleteRequest is the Schema for the deleterequests 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' + 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' + 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 @@ -72,8 +79,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: operationrequests.user.sealos.io spec: group: user.sealos.io @@ -88,6 +94,9 @@ spec: - jsonPath: .spec.action name: Action type: string + - jsonPath: .spec.namespace + name: Namespace + type: string - jsonPath: .spec.user name: User type: string @@ -97,20 +106,28 @@ spec: - jsonPath: .status.phase name: Phase type: string + - jsonPath: .metadata.creationTimestamp + name: Age + type: date name: v1 schema: openAPIV3Schema: description: Operationrequest is the Schema for the operation requests 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' + 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' + 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 @@ -123,6 +140,9 @@ spec: - Update - Deprive type: string + namespace: + description: Namespace is the workspace that needs to be operated. + type: string role: enum: - Owner @@ -155,8 +175,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.10.0 - creationTimestamp: null + controller-gen.kubebuilder.io/version: v0.14.0 name: users.user.sealos.io spec: group: user.sealos.io @@ -183,14 +202,19 @@ spec: description: User is the Schema for the users 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' + 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' + 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 @@ -199,12 +223,14 @@ spec: properties: csrExpirationSeconds: default: 7200 - description: "expirationSeconds is the requested duration of validity - of the issued certificate. The certificate signer may issue a certificate - with a different validity duration so a client must check the delta - between the notBefore and notAfter fields in the issued certificate - to determine the actual duration. \n The minimum valid value for - expirationSeconds is 600, i.e. 10 minutes." + description: |- + expirationSeconds is the requested duration of validity of the issued + certificate. The certificate signer may issue a certificate with a different + validity duration so a client must check the delta between the notBefore and + notAfter fields in the issued certificate to determine the actual duration. + + + The minimum valid value for expirationSeconds is 600, i.e. 10 minutes. format: int32 type: integer type: object @@ -315,7 +341,6 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: - creationTimestamp: null name: user-manager-role rules: - apiGroups: diff --git a/controllers/user/main.go b/controllers/user/main.go index f52448906a0..7aee541e0e2 100644 --- a/controllers/user/main.go +++ b/controllers/user/main.go @@ -161,13 +161,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "DeleteRequest") os.Exit(1) } - if err = (&controllers.AdaptRoleBindingReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "AdaptRoleBinding") - os.Exit(1) - } + //if err = (&controllers.AdaptRoleBindingReconciler{ + // Client: mgr.GetClient(), + // Scheme: mgr.GetScheme(), + //}).SetupWithManager(mgr); err != nil { + // setupLog.Error(err, "unable to create controller", "controller", "AdaptRoleBinding") + // os.Exit(1) + //} //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/deploy/cloud/scripts/init.sh b/deploy/cloud/scripts/init.sh index a46e384c9d8..70f9c07e0a3 100644 --- a/deploy/cloud/scripts/init.sh +++ b/deploy/cloud/scripts/init.sh @@ -10,9 +10,12 @@ cockroachdbGlobalUri="" localRegionUID="" tlsCrtPlaceholder="" -tlsKeyPlaceholder="" acmednsSecretPlaceholder="" + saltKey="" +jwtInternal="" +jwtRegional="" +jwtGlobal="" function prepare { # source .env @@ -36,6 +39,9 @@ function prepare { # gen regionUID if not set or not found in secret gen_regionUID + # gen jwt tokens + gen_jwt_tokens + # create tls secret create_tls_secret } @@ -132,6 +138,7 @@ function gen_cockroachdbUri() { cockroachdbGlobalUri="$cockroachdbUri/global" } +# TODO: use a better way to check saltKey function gen_saltKey() { password_salt=$(kubectl get configmap desktop-frontend-config -n sealos -o jsonpath='{.data.config\.yaml}' | grep "salt:" | awk '{print $2}' 2>/dev/null | tr -d '"' || true) if [[ -z "$password_salt" ]]; then @@ -141,6 +148,28 @@ function gen_saltKey() { fi } +# TODO: use a better way to check jwt tokens +function gen_jwt_tokens() { + jwt_internal=$(kubectl get configmap desktop-frontend-config -n sealos -o jsonpath='{.data.config\.yaml}' | grep "internal:" | awk '{print $2}' 2>/dev/null | tr -d '"' || true) + if [[ -z "$jwt_internal" ]]; then + jwtInternal=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) + if [[ -z "$jwt_regional" ]]; then + jwtRegional=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) + if [[ -z "$jwt_global" ]]; then + jwtGlobal=$(tr -dc 'a-z0-9' /dev/null | tr -d '"' || true) if [[ -z "$uid" ]]; then @@ -176,7 +205,10 @@ function sealos_run_desktop { --env regionUID="$localRegionUID" \ --env databaseMongodbURI="${mongodbUri}/sealos-auth?authSource=admin" \ --env databaseLocalCockroachdbURI="$cockroachdbLocalUri" \ - --env databaseGlobalCockroachdbURI="$cockroachdbGlobalUri" + --env databaseGlobalCockroachdbURI="$cockroachdbGlobalUri" \ + --env jwtInternal="$jwtInternal" \ + --env jwtRegional="$jwtRegional" \ + --env jwtGlobal="$jwtGlobal" } function sealos_run_controller { @@ -211,7 +243,8 @@ function sealos_run_controller { --env DEFAULT_NAMESPACE="account-system" \ --env GLOBAL_COCKROACH_URI="$cockroachdbGlobalUri" \ --env LOCAL_COCKROACH_URI="$cockroachdbLocalUri" \ - --env LOCAL_REGION="$localRegionUID" + --env LOCAL_REGION="$localRegionUID" \ + --env ACCOUNT_API_JWT_SECRET="$jwtInternal" sealos run tars/account-service.tar --env cloudDomain="$cloudDomain" --env cloudPort="$cloudPort" @@ -257,8 +290,9 @@ function sealos_run_frontend { --env cloudPort="$cloudPort" \ --env certSecretName="wildcard-cert" \ --env transferEnabled="true" \ - --env rechargeEnabled="false" - + --env rechargeEnabled="false" \ + --env jwtInternal="$jwtInternal" + echo "run template frontend" sealos run tars/frontend-template.tar \ --env cloudDomain=$cloudDomain \ diff --git a/deploy/devbox/Kubefile b/deploy/devbox/Kubefile new file mode 100644 index 00000000000..b3edf5f36b7 --- /dev/null +++ b/deploy/devbox/Kubefile @@ -0,0 +1,11 @@ +FROM scratch +COPY tars tars +COPY scripts scripts + +ENV cloudDomain=${cloudDomain:-"127.0.0.1.nip.io"} +ENV cloudPort="" +ENV registryAddr=${registryAddr:-"sealos.hub:5000"} +ENV registryUser=${registryUser:-"admin"} +ENV registryPassword=${registryPassword:-"passw0rd"} + +CMD ["bash scripts/init.sh"] diff --git a/deploy/devbox/init.sh b/deploy/devbox/init.sh new file mode 100644 index 00000000000..875547bca3d --- /dev/null +++ b/deploy/devbox/init.sh @@ -0,0 +1,33 @@ +#!/bin/bash +readonly ARCH=${1:-amd64} +set -e + +mkdir -p tars + +RetryPullImageInterval=3 +RetrySleepSeconds=3 + + +retryPullImage() { + local image=$1 + local retry=0 + local retryMax=3 + set +e + while [ $retry -lt $RetryPullImageInterval ]; do + sealos pull --policy=always --platform=linux/"${ARCH}" $image >/dev/null && break + retry=$(($retry + 1)) + echo "retry pull image $image, retry times: $retry" + sleep $RetrySleepSeconds + done + set -e + if [ $retry -eq $retryMax ]; then + echo "pull image $image failed" + exit 1 + fi +} + +retryPullImage ghcr.io/labring/sealos-cloud-devbox-controller:latest +retryPullImage ghcr.io/labring/sealos-cloud-devbox-frontend:latest + +sealos save -o tars/devbox-controller.tar ghcr.io/labring/sealos-cloud-devbox-controller:latest +sealos save -o tars/devbox-frontend.tar ghcr.io/labring/sealos-cloud-devbox-frontend:latest diff --git a/deploy/devbox/scripts/init.sh b/deploy/devbox/scripts/init.sh new file mode 100644 index 00000000000..54e779f940c --- /dev/null +++ b/deploy/devbox/scripts/init.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +sealos run tars/devbox-controller.tar -e cloudDomain=${cloudDomain} -e cloudPort=${cloudPort} -e registryAddr=${registryAddr} -e registryUser=${registryUser} -e registryPassword=${registryPassword} +sealos run tars/devbox-frontend.tar -e cloudDomain=${cloudDomain} -e cloudPort=${cloudPort} diff --git a/deploy/objectstorage/Kubefile b/deploy/objectstorage/Kubefile index c8de4664538..01c581d7cfe 100644 --- a/deploy/objectstorage/Kubefile +++ b/deploy/objectstorage/Kubefile @@ -11,6 +11,6 @@ ENV minioStorageSize=${minioStorageSize:-1Gi} ENV promStorageSize=${promStorageSize:-1Gi} ENV minioAdminUser=${minioAdminUser:-"username"} ENV minioAdminPassword=${minioAdminPassword:-"passw0rd"} -ENV minioKubeblocksPassword=${minioAdminPassword:-"kubeblocks"} +ENV minioKubeblocksPassword=${minioKubeblocksPassword:-"kubeblocks"} CMD ["bash scripts/init.sh"] diff --git a/deploy/objectstorage/README.md b/deploy/objectstorage/README.md index a7c9bd9c561..9e0fee22bcb 100644 --- a/deploy/objectstorage/README.md +++ b/deploy/objectstorage/README.md @@ -2,7 +2,13 @@ ## version -date: 2024.9.10 +### date: 2024.10.16 + +log: fix minioKubeblocksPassword env error + +### date: 2024.9.10 + +log: update controller and frontend image ## components diff --git a/deploy/objectstorage/init.sh b/deploy/objectstorage/init.sh index 5fbe719077d..9e1b22eba1c 100644 --- a/deploy/objectstorage/init.sh +++ b/deploy/objectstorage/init.sh @@ -1,6 +1,6 @@ #!/bin/bash set -e -export readonly ARCH=${1:-amd64} +readonly ARCH=${1:-amd64} mkdir -p tars RetryPullImageInterval=3 diff --git a/docs/4.0/docs/self-hosting/sealos/installation.md b/docs/4.0/docs/self-hosting/sealos/installation.md index 74725f792de..6f146822581 100644 --- a/docs/4.0/docs/self-hosting/sealos/installation.md +++ b/docs/4.0/docs/self-hosting/sealos/installation.md @@ -43,8 +43,8 @@ We advise using Ubuntu 22.04 LTS with a kernel version of 5.4 or higher. The spe |------------------|----------------|-----|--------|---------|------------|-------| | Ubuntu 22.04 LTS | ≥ 5.4 | 8C | 16GB | 100GB | Odd Number | Any | -:::info -Kubernetes and Sealos Cloud Operating system require roughly 2 cores (2c) and 2GB of memory (2g) per Master node, and about 1 core (1c) and 1GB of memory (1g) per Node node. Ensure each node in your cluster is well-equipped for these system components. +:::info +Kubernetes and Sealos Cloud Operating system require roughly 2 cores (2c) and 2GB of memory (2g) per Master node, and about 1 core (1c) and 1GB of memory (1g) per Node node. Ensure each node in your cluster is well-equipped for these system components. ::: ### Network Considerations @@ -83,14 +83,14 @@ There are different installation options based on your domain name needs: ### 1. No public domain, don't want custom domains -If you don't have a public domain or need custom domains, you can use the free `nip.io` wildcard DNS service. `nip.io` maps dynamic IP addresses to fixed subdomains, useful for local dev environments. +If you don't have a public domain or need custom domains, you can use the free `nip.io` wildcard DNS service. `nip.io` maps dynamic IP addresses to fixed subdomains, useful for local dev environments. -It works by taking any IP address as part of a `nip.io` subdomain, and resolving requests back to that IP. For example, if your intranet IP is `192.168.1.10`, you can use the domain `192.168.1.10.nip.io`. Requests to this domain will resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up intranet DNS. +It works by taking any IP address as part of a `nip.io` subdomain, and resolving requests back to that IP. For example, if your intranet IP is `192.168.1.10`, you can use the domain `192.168.1.10.nip.io`. Requests to this domain will resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up intranet DNS. -To use nip.io for Sealos, run the below on the first master node and enter prompts: +To use nip.io for Sealos, run the below on the first master node and enter prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh ``` When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP. @@ -105,112 +105,112 @@ admin Username: admin admin Password: sealos2023 ``` -### 2. Have public domain, want public access +### 2. Have public domain, want public access If you have a public domain and want public Sealos access, you'll need a trusted public SSL/TLS certificate. You can use acme.sh to automatically issue certs, or get free certs from your domain provider or purchase commercial certificates. -Place the certificate files in a directory on the first master, like `/root/certs/`. +Place the certificate files in a directory on the first master, like `/root/certs/`. :::info -You'll also need to configure DNS records at your domain provider: +You'll also need to configure DNS records at your domain provider: -``` -cloud.example.io A +``` +cloud.example.io A *.cloud.example.io A -``` +``` -This maps your domain and subdomains to the first master's public IP. +This maps your domain and subdomains to the first master's public IP. -::: +::: -Then run below on the first master, entering prompts: +Then run below on the first master, entering prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= \ ---cert-path= \ +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= \ +--cert-path= \ --key-path= -``` +``` -+ `` is your public domain ++ `` is your public domain + `` is the certificate file path (`.crt` or `.pem`) e.g. `/root/certs/example.crt` + `` is the private key file path (`.key` or `.pem`) e.g. `/root/certs/example.key` -### 3. Have public domain, want internal access +### 3. Have public domain, want internal access -If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS records resolving to the first master's internal IP: +If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS records resolving to the first master's internal IP: -``` -cloud.example.io A -*.cloud.example.io A -``` +``` +cloud.example.io A +*.cloud.example.io A +``` -Then run the below on the first master, entering prompts: +Then run the below on the first master, entering prompts: -```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= -``` +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` -Where `` is your public domain. +Where `` is your public domain. The installer will use [cert-manager](https://cert-manager.io/docs/) to automatically sign certificates. -### 4. No public domain, want custom domain +### 4. No public domain, want custom domain -If you don't have a public domain but need a custom domain, set up internal DNS resolving a custom domain to the first master's internal IP. +If you don't have a public domain but need a custom domain, set up internal DNS resolving a custom domain to the first master's internal IP. :::note -Assuming the first master internal IP is `192.168.1.10`, and your custom domain is `cloud.example.io`. +Assuming the first master internal IP is `192.168.1.10`, and your custom domain is `cloud.example.io`. -::: +::: You can use CoreDNS, Reference configuration: -``` +``` (global_cache) { cache { # [5, 60] success 65536 3600 300 # [1, 10] - denial 8192 600 60 + denial 8192 600 60 prefetch 1 60m 10% - } -} + } +} .:53 { errors - health - ready + health + ready import global_cache - + template IN A cloud.example.io { answer "{{ .Name }} 60 IN A 192.168.1.10" - fallthrough + fallthrough } - + forward . 223.5.5.5 - + log loop - + reload 6s } -``` +``` -This resolves `cloud.example.io` and subdomains to the first master internal IP. +This resolves `cloud.example.io` and subdomains to the first master internal IP. -Then run below on the first master, entering prompts: +Then run below on the first master, entering prompts: -```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= -``` +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` -Where `` is your custom domain. +Where `` is your custom domain. The installer uses [cert-manager](https://cert-manager.io/docs/) for certificates. diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md b/docs/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md index 81e46c7d4ed..630413ae83e 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/lifecycle-management.md @@ -72,7 +72,7 @@ $ sealos run labring/redis-operator:3.1.4 For cluster images not available in the Sealos ecosystem, users can easily build and customize their own cluster images. For example: -[Building an Ingress Cluster Image](/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md) +[Building an Ingress Cluster Image](/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md) You can also customize your own Kubernetes cluster: diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md index e0acd3af8fb..abf24cf22a6 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md @@ -10,7 +10,7 @@ Sealos supports installing Kubernetes clusters on `amd64` and `arm64` architectu ## Prerequisites -You'll first need to [download the Sealos CLI tool](/self-hosting/lifecycle-management/quick-start/install-cli.md). +You'll first need to [download the Sealos CLI tool](/developer-guide/lifecycle-management/quick-start/install-cli.md). Sealos is a simple Golang binary that can be installed on most Linux operating systems. Here are some basic installation requirements: diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md index 6131d3fe81b..4c79c068f7b 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/apply.md @@ -82,7 +82,7 @@ sealos apply -f Clusterfile --values values.yaml This command will apply the `Clusterfile` based on the values in the `values.yaml` file. -**For more examples, please refer to the [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster/.md) +**For more examples, please refer to the [Run Cluster](/developer-guide/lifecycle-management/operations/run-cluster/.md) section.** That's it for the usage guide of the `sealos apply` command. We hope this helps you. If you have any questions or diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md index 67df5a4b660..665e2281fd4 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/build.md @@ -76,12 +76,12 @@ process, greatly improving the convenience and efficiency of building images. Here are some detailed examples: -- [Build with Image Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-image_list.md) -- [Build with Deploy Manifests](/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md) -- [Build with Helm Charts](/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md) -- [Build with Binary](/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md) -- [Build with go-template](/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md) -- [Build with exec and scp](/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md) +- [Build with Image Manifests](/developer-guide/lifecycle-management/operations/build-image/build-image-image_list.md) +- [Build with Deploy Manifests](/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md) +- [Build with Helm Charts](/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [Build with Binary](/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md) +- [Build with go-template](/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md) +- [Build with exec and scp](/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md) With the `build` command in Sealos, you can build OCI images based on various instruction files to provide the required images for Sealos. This process includes handling various instructions in Dockerfiles or other instruction files, such diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md index b9739326dd7..f387f4fb910 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/commands.md @@ -57,5 +57,5 @@ The `--debug` flag in Sealos is a global flag used to enable debug mode for more operation when issues occur. For installation instructions, please refer to -the [Sealos Installation Guide](/self-hosting/lifecycle-management/quick-start/installation); for a quick start guide, -please refer to the [Quick Start Guide](/self-hosting/lifecycle-management/quick-start/.md). +the [Sealos Installation Guide](/developer-guide/lifecycle-management/quick-start/installation); for a quick start guide, +please refer to the [Quick Start Guide](/developer-guide/lifecycle-management/quick-start/.md). diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md index a9e358e84de..510cf229103 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/gen.md @@ -50,7 +50,7 @@ command to create or update the cluster based on this configuration file. Example explanations: -- [Custom Configuration Installation](/self-hosting/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) +- [Custom Configuration Installation](/developer-guide/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) That's the usage guide for the `sealos gen` command, and we hope it has been helpful. If you encounter any problems during usage, feel free to ask us. diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md index 0fbb627dc2b..1326a68652d 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md @@ -31,4 +31,4 @@ multi-architecture Docker or OCI images. Users can create custom manifest lists convenient to deploy and run Docker images on different hardware architectures. Users who want to build multi-architecture images through the manifest command can refer to the -document [Building Cluster Images that Support Multiple Architectures](/self-hosting/lifecycle-management/operations/build-image/build-multi-arch-image.md). +document [Building Cluster Images that Support Multiple Architectures](/developer-guide/lifecycle-management/operations/build-image/build-multi-arch-image.md). diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md index 1c0ad8589f0..c0d3089052c 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/commands/run.md @@ -88,4 +88,4 @@ sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.1 These examples demonstrate the power and flexibility of the `sealos run` command, which can be customized and adjusted according to your needs. -For more examples, please refer to [Run Cluster](/self-hosting/lifecycle-management/operations/run-cluster.md). +For more examples, please refer to [Run Cluster](/developer-guide/lifecycle-management/operations/run-cluster.md). diff --git a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md index 4982be8a0c9..e62e147d722 100644 --- a/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md +++ b/docs/5.0/docs/developer-guide/lifecycle-management/reference/sealos/sealos.md @@ -13,7 +13,7 @@ images, and perform other functions. Below are detailed introductions to these c This chapter provides a user guide for using Sealos, as well as information related to cluster images. - Sealos User Guide: For detailed information about each command, including all supported parameters and subcommands, - please refer to the [sealos](/self-hosting/lifecycle-management/reference/sealos/commands.md) reference documentation. + please refer to the [sealos](/developer-guide/lifecycle-management/reference/sealos/commands.md) reference documentation. - Cluster Images: - - [Rootfs Cluster Images](/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) - - [Application Cluster Images](/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md) + - [Rootfs Cluster Images](/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application Cluster Images](/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md) diff --git a/docs/5.0/docs/developer-guide/sealos/installation.md b/docs/5.0/docs/developer-guide/sealos/installation.md index 8fab95e05c7..65948142f03 100644 --- a/docs/5.0/docs/developer-guide/sealos/installation.md +++ b/docs/5.0/docs/developer-guide/sealos/installation.md @@ -87,7 +87,7 @@ This script is specifically designed for deploying Sealos clusters on "Kubernete not compatible with Kubernetes clusters set up in other ways. For instructions on deploying Kubernetes clusters using Sealos, refer -to: [Installing Kubernetes Clusters](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md). This method +to: [Installing Kubernetes Clusters](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md). This method is compatible with most Kubernetes versions available on [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags), except for version 1.28 and higher. ::: @@ -109,7 +109,7 @@ resolve to `192.168.1.10`, eliminating the need to modify local hosts or set up To use nip.io for Sealos, run the below on the first master node and enter prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh ``` When prompted for the Sealos Cloud domain name, use a format like `[ip].nip.io`, where [ip] is your Master node's IP. @@ -135,10 +135,10 @@ Place the certificate files in a directory on the first master, like `/root/cert You'll also need to configure DNS records at your domain provider: -``` -cloud.example.io A +``` +cloud.example.io A *.cloud.example.io A -``` +``` This maps your domain and subdomains to the first master's public IP. @@ -147,11 +147,11 @@ This maps your domain and subdomains to the first master's public IP. Then run below on the first master, entering prompts: ```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= \ ---cert-path= \ +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= \ +--cert-path= \ --key-path= -``` +``` + `` is your public domain + `` is the certificate file path (`.crt` or `.pem`) e.g. `/root/certs/example.crt` @@ -162,17 +162,17 @@ $ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/script If you have a public domain but only internal IPs, or only want internal Sealos access, you just need to configure DNS records resolving to the first master's internal IP: -``` -cloud.example.io A -*.cloud.example.io A -``` +``` +cloud.example.io A +*.cloud.example.io A +``` Then run the below on the first master, entering prompts: -```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= -``` +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` Where `` is your public domain. @@ -191,46 +191,46 @@ Assuming the first master internal IP is `192.168.1.10`, and your custom domain You can use CoreDNS, Reference configuration: -``` +``` (global_cache) { cache { # [5, 60] success 65536 3600 300 # [1, 10] - denial 8192 600 60 + denial 8192 600 60 prefetch 1 60m 10% - } -} + } +} .:53 { errors - health - ready + health + ready import global_cache - + template IN A cloud.example.io { answer "{{ .Name }} 60 IN A 192.168.1.10" - fallthrough + fallthrough } - + forward . 223.5.5.5 - + log loop - + reload 6s } -``` +``` This resolves `cloud.example.io` and subdomains to the first master internal IP. Then run below on the first master, entering prompts: -```bash -$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.0-beta5/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ ---cloud-domain= -``` +```bash +$ curl -sfL https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ +--cloud-domain= +``` Where `` is your custom domain. diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md index c5d05e571b2..9ade8de8d26 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/lifecycle-management.md @@ -64,7 +64,7 @@ $ sealos run labring/redis-operator:3.1.4 对于 Sealos 生态没有的集群镜像,用户可以方便地自己构建和定制属于自己的集群镜像。例如: -[构建一个 ingress 集群镜像](/self-hosting/lifecycle-management/quick-start/build-ingress-cluster-image.md) +[构建一个 ingress 集群镜像](/developer-guide/lifecycle-management/quick-start/build-ingress-cluster-image.md) 您还可以定制一个完全属于自己的 Kubernetes: diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md index 7d75fb25586..9079de5dea3 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md @@ -10,7 +10,7 @@ Sealos 支持安装 `amd64` 和 `arm64` 架构的 K8s 集群。 ## 先决条件 -首先需要[下载 Sealos 命令行工具](/self-hosting/lifecycle-management/quick-start/install-cli.md),sealos 是一个简单的 Golang 二进制文件,可以安装在大多数 Linux 操作系统中。 +首先需要[下载 Sealos 命令行工具](/developer-guide/lifecycle-management/quick-start/install-cli.md),sealos 是一个简单的 Golang 二进制文件,可以安装在大多数 Linux 操作系统中。 以下是一些基本的安装要求: diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md index 878cdb44e2a..59a78822922 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/quick-start/install-cli.md @@ -97,5 +97,5 @@ $ make build BINS=sealos ## 下一步 -[安装 K8s 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md)。 +[安装 K8s 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md)。 diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md index 4b0aac5a94c..cd0de789994 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/apply.md @@ -81,6 +81,6 @@ sealos apply -f Clusterfile --values values.yaml 这条命令会根据 `values.yaml` 文件中的值应用 `Clusterfile`。 -**更多示例请参考[启动镜像](/self-hosting/lifecycle-management/operations/run-cluster/)** +**更多示例请参考[启动镜像](/developer-guide/lifecycle-management/operations/run-cluster/)** 以上就是 `sealos apply` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md index ec01d976a16..9420f6d6149 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/build.md @@ -59,12 +59,12 @@ sealos build -t myapp:v1.0.0 -f Dockerfile . 下面有一些详细的示例: -- [基于镜像清单构建](/self-hosting/lifecycle-management/operations/build-image/build-image-image_list.md) -- [基于部署清单构建](/self-hosting/lifecycle-management/operations/build-image/build-image-manifests.md) -- [基于helm-charts构建](/self-hosting/lifecycle-management/operations/build-image/build-image-helm_charts.md) -- [基于二进制构建](/self-hosting/lifecycle-management/operations/build-image/build-image-binary.md) -- [基于go-template构建](/self-hosting/lifecycle-management/operations/build-image/build-image-go_template.md) -- [基于exec和scp构建](/self-hosting/lifecycle-management/operations/build-image/build-image-scp_exec.md) +- [基于镜像清单构建](/developer-guide/lifecycle-management/operations/build-image/build-image-image_list.md) +- [基于部署清单构建](/developer-guide/lifecycle-management/operations/build-image/build-image-manifests.md) +- [基于helm-charts构建](/developer-guide/lifecycle-management/operations/build-image/build-image-helm_charts.md) +- [基于二进制构建](/developer-guide/lifecycle-management/operations/build-image/build-image-binary.md) +- [基于go-template构建](/developer-guide/lifecycle-management/operations/build-image/build-image-go_template.md) +- [基于exec和scp构建](/developer-guide/lifecycle-management/operations/build-image/build-image-scp_exec.md) 通过 Sealos `build` 命令,可以基于多种指令文件构建 OCI 镜像,为sealos提供所需的镜像。这个过程包括处理 Dockerfile 或其他指令文件中的各种指令,如 `FROM`、`RUN`、`ADD` 等,以及处理镜像层次、镜像标签等。构建过程也包括拉取基础镜像、运行命令、保存结果等步骤。每一个步骤都可以通过上述的选项进行详细的控制和定制,以满足不同的构建需求。 diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md index d64a6ad183a..8d0b4f0aeff 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/commands.md @@ -55,4 +55,4 @@ description: 了解Sealos命令,包括Kubernetes集群管理、节点管理、 Sealos 的 `--debug` 参数是一个全局参数,用于开启调试模式,以便在出现问题时能更详细地了解系统的运行情况。 -有关安装说明,请参见[下载 Sealos 命令行工具](/self-hosting/lifecycle-management/quick-start/install-cli.md); 如需安装 Kubernetes 集群,请参见[安装 Kubernetes 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md)。 \ No newline at end of file +有关安装说明,请参见[下载 Sealos 命令行工具](/developer-guide/lifecycle-management/quick-start/install-cli.md); 如需安装 Kubernetes 集群,请参见[安装 Kubernetes 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md)。 \ No newline at end of file diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md index 0d87d0607b2..c6e0ef99777 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/gen.md @@ -46,6 +46,6 @@ Sealos 的 `gen` 命令是用于生成 Kubernetes 集群的配置文件(Cluste 示例说明: -- [自定义配置安装](/self-hosting/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) +- [自定义配置安装](/developer-guide/lifecycle-management/operations/run-cluster/gen-apply-cluster.md) 以上就是 `sealos gen` 命令的使用指南,希望对你有所帮助。如果你在使用过程中遇到任何问题,欢迎向我们提问。 diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md index c09226bbd43..0d638010a11 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/manifest.md @@ -19,4 +19,4 @@ Sealos 的 `manifest` 命令用于创建、修改和推送 manifest 列表和镜 通过 `sealos manifest` 命令,可以灵活地管理 manifest 列表或镜像索引,为多架构的 Docker 或 OCI 镜像提供支持。用户可以根据自己的需求,创建自定义的 manifest 列表,方便在不同的硬件架构上部署和运行 Docker 镜像。 -用户如果想通过manifest命令构建多架构镜像,可以参考文档[构建支持多架构的集群镜像](/self-hosting/lifecycle-management/operations/build-image/build-multi-arch-image.md) +用户如果想通过manifest命令构建多架构镜像,可以参考文档[构建支持多架构的集群镜像](/developer-guide/lifecycle-management/operations/build-image/build-multi-arch-image.md) diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md index 4901bda34b1..d5a44283aaa 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/commands/run.md @@ -83,4 +83,4 @@ sealos run -e DashBoardPort=8443 mydashboard:latest --masters 192.168.0.2,192.1 这些示例展示了 `sealos run` 命令的强大和灵活性,可以根据您的需求进行定制和调整。 -更多示例请参考 [运行集群](/self-hosting/lifecycle-management/operations/run-cluster.md)。 +更多示例请参考 [运行集群](/developer-guide/lifecycle-management/operations/run-cluster.md)。 diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md index 9d17e9bcae1..5948b3cd081 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/lifecycle-management/reference/sealos/sealos.md @@ -10,9 +10,9 @@ Sealos 是一个统一的云操作系统,用于管理云原生应用。它提 本章节主要介绍 Sealos 的使用指南以及集群镜像相关说明。 -- Sealos使用指南: 有关每个命令的详细信息,包括所有受支持的参数和子命令, 请参阅 [sealos](/self-hosting/lifecycle-management/reference/sealos/commands.md) 参考文档。 +- Sealos使用指南: 有关每个命令的详细信息,包括所有受支持的参数和子命令, 请参阅 [sealos](/developer-guide/lifecycle-management/reference/sealos/commands.md) 参考文档。 - 集群镜像: - - [Rootfs集群镜像](/self-hosting/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) - - [Application集群镜像](/self-hosting/lifecycle-management/reference/sealos/app-cluster-image.md) + - [Rootfs集群镜像](/developer-guide/lifecycle-management/reference/sealos/kubernetes-cluster-image.md) + - [Application集群镜像](/developer-guide/lifecycle-management/reference/sealos/app-cluster-image.md) diff --git a/docs/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md b/docs/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md index c8808661e09..6da7063433f 100644 --- a/docs/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md +++ b/docs/5.0/i18n/zh-Hans/developer-guide/sealos/installation.md @@ -84,7 +84,7 @@ Sealos 需要使用证书来保证通信安全,默认在您不提供证书的 该脚本只支持在 “使用 Sealos 安装的 Kubernetes 集群” 上部署 Sealos 集群,暂不支持其他方式部署的 Kubernetes。 关于如何使用 Sealos 部署 Kubernetes -集群,可以参考:[安装 Kubernetes 集群](/self-hosting/lifecycle-management/quick-start/deploy-kubernetes.md) +集群,可以参考:[安装 Kubernetes 集群](/developer-guide/lifecycle-management/quick-start/deploy-kubernetes.md) ,支持 [Docker Hub](https://hub.docker.com/r/labring/kubernetes/tags) 中的几乎所有 Kubernetes 版本(**暂不支持 1.28 及以上版本 **)。 @@ -102,9 +102,9 @@ Sealos 需要使用证书来保证通信安全,默认在您不提供证书的 使用 nip.io 作为 Sealos 的域名非常简单,只需在第一个 Master 节点上执行以下命令,并根据提示输入参数: -```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0 \ +```bash +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com ``` @@ -147,8 +147,8 @@ cloud.example.io A 192.168.1.10 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= \ @@ -174,8 +174,8 @@ cloud.example.io A 192.168.1.10 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= @@ -233,8 +233,8 @@ $ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring 然后在第一个 Master 节点上执行以下命令,并根据提示输入参数: ```bash -$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.0/scripts/cloud/install.sh -o /tmp/install.sh && bash /tmp/install.sh \ - --cloud-version=v5.0.0 \ +$ curl -sfL https://mirror.ghproxy.com/https://raw.githubusercontent.com/labring/sealos/v5.0.1/scripts/cloud/install.sh -o /tmp/install.sh && SEALOS_VERSION=v5.0.1 && bash /tmp/install.sh \ + --cloud-version=v5.0.1 \ --image-registry=registry.cn-shanghai.aliyuncs.com --zh \ --proxy-prefix=https://mirror.ghproxy.com \ --cloud-domain= @@ -355,7 +355,7 @@ Linux 不同发行版更新根证书存储的命令不一样,用来保存私 # update-ca-certificates 会添加 /etc/ca-certificates.conf 配置文件中指定的证书 # 另外所有 /usr/local/share/ca-certificates/*.crt 会被列为隐式信任 $ sudo update-ca-certificates - + # - 删除 $ sudo rm /usr/local/share/ca-certificates/root_ca.crt $ sudo update-ca-certificates --fresh @@ -431,4 +431,4 @@ Linux 不同发行版更新根证书存储的命令不一样,用来保存私 选择刚刚下载的 License 文件进行上传,然后点击右下角的「激活 License」,便可激活 License。 - ![](images/sealos-cost-center.jpg) \ No newline at end of file + ![](images/sealos-cost-center.jpg) diff --git a/docs/website/docusaurus.config.js b/docs/website/docusaurus.config.js index 19dc5010ad9..8ba2a26c79c 100644 --- a/docs/website/docusaurus.config.js +++ b/docs/website/docusaurus.config.js @@ -251,11 +251,7 @@ const config = { { src: "/global.js", async: true - }, - ...(isDomesticSite ? [{ - src: 'https://hm.baidu.com/hm.js?d8e8ecf669c47dc2512d3f1417e761f9', - async: true, - }] : []) + } ], headTags: [ { @@ -279,6 +275,101 @@ const config = { }, } }, + function gtmPlugin (context, options) { + return { + name: 'docusaurus-gtm-plugin', + injectHtmlTags () { + return { + headTags: [ + { + tagName: 'script', + innerHTML: ` + (function() { + const hostname = window.location.hostname; + if (hostname !== 'sealos.run') { + (function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': + new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], + j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= + 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); + })(window,document,'script','dataLayer','GTM-5953N4CP'); + } + })(); + `, + }, + ], + preBodyTags: [ + { + tagName: 'script', + innerHTML: ` + if (window.location.hostname !== 'sealos.run') { + document.write(''); + } + `, + }, + ] + } + }, + } + }, + function umamiPlugin (context, options) { + return { + name: 'docusaurus-umami-plugin', + injectHtmlTags () { + return { + headTags: [ + { + tagName: 'script', + innerHTML: ` + (function() { + const hostname = window.location.hostname; + if (hostname === 'sealos.run') { + const script1 = document.createElement('script'); + script1.src = 'https://umami.cloud.sealos.io/oishii'; + script1.setAttribute('data-website-id', 'e5a8009f-7cb6-4841-9522-d23b96216b7a'); + script1.async = true; + document.head.appendChild(script1); + } else { + const script2 = document.createElement('script'); + script2.src = 'https://umami.cloud.sealos.io/oishii'; + script2.setAttribute('data-website-id', 'a1c29ace-b288-431a-a2eb-8617d1d5b5ed'); + script2.async = true; + document.head.appendChild(script2); + } + })(); + `, + }, + ], + } + }, + } + }, + function baiduPlugin (context, options) { + return { + name: 'docusaurus-baidu-plugin', + injectHtmlTags () { + return { + headTags: [ + { + tagName: 'script', + innerHTML: ` + (function() { + const hostname = window.location.hostname; + if (hostname === 'sealos.run') { + var _hmt = _hmt || []; + var hm = document.createElement("script"); + hm.src = "https://hm.baidu.com/hm.js?d8e8ecf669c47dc2512d3f1417e761f9"; + hm.async = true; + var s = document.getElementsByTagName("script")[0]; + s.parentNode.insertBefore(hm, s); + } + })(); + `, + }, + ], + } + }, + } + } ] } diff --git a/docs/website/src/components/SaleBanner/index.tsx b/docs/website/src/components/SaleBanner/index.tsx index cc5c8437a32..9bd6c86363b 100644 --- a/docs/website/src/components/SaleBanner/index.tsx +++ b/docs/website/src/components/SaleBanner/index.tsx @@ -13,11 +13,8 @@ export default function SaleBanner() { setIsBannerVisible(false); }; - const goDetailFeishu = () => { - window.open( - `https://fael3z0zfze.feishu.cn/wiki/SzKowEuQji5coRkm5o8cm8oJn3L?from=from_copylink`, - '_blank' - ); + const goDetail = () => { + window.open(`https://mp.weixin.qq.com/s/jzOfviMgXD85r2zMQWskvA`, '_blank'); }; useEffect(() => { @@ -36,8 +33,8 @@ export default function SaleBanner() { return ( <>
- 🎉Sealos 6.18 福利大放送!充值优惠限时开启,多充多送还有精美周边! -
+ 🎉 1024 程序员节福利 Sealos 免费送你云资源 +
活动详情
@@ -50,13 +47,13 @@ export default function SaleBanner() { Sealos
-
🎉Sealos 6.18 福利大放送!
-
充值优惠限时开启
-
多充多送还有精美周边!
+
🎉 Sealos 免费送你云资源
+
1024 充值优惠限时开启
+
体验Devbox,即送20余额
{ - window.open(`${cloudUrl}/?openapp=system-costcenter?openRecharge=true`, '_blank'); + window.open(`${cloudUrl}/?openapp=system-devbox?openRecharge=true`, '_blank'); closeBanner(); }} > diff --git a/docs/website/src/pages/index.tsx b/docs/website/src/pages/index.tsx index 7e5c5ebef25..519f0a6cfe6 100644 --- a/docs/website/src/pages/index.tsx +++ b/docs/website/src/pages/index.tsx @@ -1,7 +1,6 @@ import '@site/src/css/animate.css'; import Layout from '@theme/Layout'; import React, { useEffect, useMemo } from 'react'; -import { Helmet } from 'react-helmet'; import { PC_MIN_WIDTH } from '../constants/platform'; import useWindow from '../hooks/useWindow'; import Capability from './components/Capability'; @@ -20,26 +19,6 @@ const Home = () => { const isPc = useMemo(() => screenWidth > PC_MIN_WIDTH, [screenWidth]); const { i18n } = useDocusaurusContext(); - useEffect(() => { - const loadUmamiScript = () => { - const hostname = window.location.hostname; - if (hostname === 'sealos.run') { - const script1 = document.createElement('script'); - script1.src = 'https://umami.cloud.sealos.io/oishii'; - script1.setAttribute('data-website-id', 'e5a8009f-7cb6-4841-9522-d23b96216b7a'); - script1.async = true; - document.head.appendChild(script1); - } else { - const script2 = document.createElement('script'); - script2.src = 'https://umami.cloud.sealos.io/oishii'; - script2.setAttribute('data-website-id', 'a1c29ace-b288-431a-a2eb-8617d1d5b5ed'); - script2.async = true; - document.head.appendChild(script2); - } - }; - loadUmamiScript(); - }, []); - useEffect(() => { const urlParams = new URLSearchParams(window.location.search); const params: Record = {}; @@ -83,20 +62,9 @@ const Home = () => { } /> - - -
+ {/* */} diff --git a/frontend/desktop/.gitignore b/frontend/desktop/.gitignore index 0ee16aefb7b..3d4d46cf7ef 100644 --- a/frontend/desktop/.gitignore +++ b/frontend/desktop/.gitignore @@ -42,4 +42,5 @@ yalc.lock config.yaml .env +data/config.local.yaml #/prisma/region/generated/ diff --git a/frontend/desktop/.prettierrc.js b/frontend/desktop/.prettierrc.js index b070d785ebd..3e3db788f64 100644 --- a/frontend/desktop/.prettierrc.js +++ b/frontend/desktop/.prettierrc.js @@ -19,10 +19,10 @@ module.exports = { endOfLine: 'lf', overrides: [ { - files: 'config.yaml.local', + files: 'config.local.yaml', options: { parser: 'yaml' } } ] -}; +} diff --git a/frontend/desktop/data/config.yaml b/frontend/desktop/data/config.yaml index e19c486112f..6cf20f8afbe 100644 --- a/frontend/desktop/data/config.yaml +++ b/frontend/desktop/data/config.yaml @@ -4,6 +4,8 @@ cloud: regionUID: "thisiaregionuid" certSecretName: "wildcard-cert" common: + enterpriseSupportingMaterials: "" + enterpriseRealNameAuthEnabled: false realNameAuthEnabled: false guideEnabled: false apiEnabled: false @@ -67,3 +69,10 @@ desktop: # authURL: "{{ .oauth2AuthURL }}" # tokenURL: "{{ .oauth2TokenURL }}" # userInfoURL: "{{ .oauth2UserInfoURL }}" +realNameOSS: + accessKey: "" + accessKeySecret: "" + endpoint: "" + realNameBucket: "" + enterpriseRealNameBucket: "" + ssl: true diff --git a/frontend/desktop/deploy/Kubefile b/frontend/desktop/deploy/Kubefile index cb8685bde5f..fe24acf60e3 100644 --- a/frontend/desktop/deploy/Kubefile +++ b/frontend/desktop/deploy/Kubefile @@ -13,5 +13,8 @@ ENV databaseMongodbURI="" ENV databaseGlobalCockroachdbURI="" ENV databaseLocalCockroachdbURI="" ENV passwordSalt="randomSalt" +ENV jwtInternal="" +ENV jwtRegional="" +ENV jwtGlobal="" CMD ["bash scripts/init.sh"] diff --git a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl index 77c3634b860..dd6d744efd6 100644 --- a/frontend/desktop/deploy/manifests/configmap.yaml.tmpl +++ b/frontend/desktop/deploy/manifests/configmap.yaml.tmpl @@ -39,13 +39,14 @@ data: proxyAddress: "" callbackURL: "https://{{ .cloudDomain }}{{ if .cloudPort }}:{{ .cloudPort }}{{ end }}/callback" signUpEnabled: true + billingUrl: "http://account-service.account-system.svc:2333" baiduToken: "" invite: enabled: false jwt: - internal: "" - regional: "" - global: "" + internal: "{{ .jwtInternal }}" + regional: "{{ .jwtRegional }}" + global: "{{ .jwtGlobal }}" idp: password: enabled: true diff --git a/frontend/desktop/deploy/manifests/rbac.yaml b/frontend/desktop/deploy/manifests/rbac.yaml index a42c1180a4f..5a3e98d49e7 100644 --- a/frontend/desktop/deploy/manifests/rbac.yaml +++ b/frontend/desktop/deploy/manifests/rbac.yaml @@ -89,6 +89,9 @@ rules: - apiGroups: ["notification.sealos.io"] resources: ["notifications"] verbs: ["list", "get", "create", "update", "patch", "watch"] + - apiGroups: [""] + resources: ["namespaces"] + verbs: ["patch"] --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/frontend/desktop/deploy/scripts/init.sh b/frontend/desktop/deploy/scripts/init.sh index b6ebb7a1476..2c2113eccfe 100644 --- a/frontend/desktop/deploy/scripts/init.sh +++ b/frontend/desktop/deploy/scripts/init.sh @@ -6,8 +6,5 @@ if [[ -n "$cm_exists" ]]; then echo "desktop-frontend-config already exists, skip create desktop config" else echo "create desktop config" - sed -i -e "s;;$(tr -cd 'a-z0-9' ;$(tr -cd 'a-z0-9' ;$(tr -cd 'a-z0-9' () => ApiResp<{ info: { realName?: string; + enterpriseVerificationStatus?: string; + enterpriseRealName?: string; userRestrictedLevel?: number; uid: string; createdAt: Date; @@ -158,9 +160,29 @@ export const _checkRemainResource = (request: AxiosInstance) => () => export const _forceDeleteUser = (request: AxiosInstance) => (data: { code: string }) => request.post>('/api/auth/delete/force', data); -export const _realNameAuthRequest = - (request: AxiosInstance) => (data: { name: string; phone?: string; idCard: string }) => - request.post>('/api/account/realNameAuth', data); + +export const _faceAuthGenerateQRcodeUriRequest = (request: AxiosInstance) => () => + request.get>( + '/api/account/generateRealNameQRcodeUri' + ); + +export const _getFaceAuthStatusRequest = (request: AxiosInstance) => (data: { bizToken: string }) => + request.post>( + '/api/account/getFaceAuthStatus', + data + ); + +export const _enterpriseRealNameAuthRequest = (request: AxiosInstance) => (data: FormData) => { + return request.post>( + '/api/account/enterpriseRealNameAuth', + data, + { + headers: { + 'Content-Type': 'multipart/form-data' + } + } + ); +}; export const _getAmount = (request: AxiosInstance) => () => request>('/api/account/getAmount'); @@ -189,6 +211,8 @@ export const deleteUserRequest = _deleteUser(request); export const checkRemainResource = _checkRemainResource(request); export const forceDeleteUser = _forceDeleteUser(request); -export const realNameAuthRequest = _realNameAuthRequest(request); +export const enterpriseRealNameAuthRequest = _enterpriseRealNameAuthRequest(request); +export const faceAuthGenerateQRcodeUriRequest = _faceAuthGenerateQRcodeUriRequest(request); +export const getFaceAuthStatusRequest = _getFaceAuthStatusRequest(request); export const getAmount = _getAmount(request); diff --git a/frontend/desktop/src/api/platform.ts b/frontend/desktop/src/api/platform.ts index 723f8b48a9d..5549c306adb 100644 --- a/frontend/desktop/src/api/platform.ts +++ b/frontend/desktop/src/api/platform.ts @@ -8,7 +8,7 @@ import { CommonClientConfigType, TNotification } from '@/types'; -import { AccountCRD } from '@/types/user'; +import { UserTask } from '@/types/task'; // handle baidu export const uploadConvertData = ({ newType, bdVid }: { newType: number[]; bdVid?: string }) => { @@ -24,12 +24,16 @@ export const uploadConvertData = ({ newType, bdVid }: { newType: number[]; bdVid }); }; -export const updateDesktopGuide = () => { - return request.post('/api/account/updateGuide'); +export const getUserTasks = () => { + return request.get('/api/account/getTasks'); }; -export const getUserAccount = () => { - return request.get('/api/account/getAccount'); +export const checkUserTask = () => { + return request.get('/api/account/checkTask'); +}; + +export const updateTask = (taskId: string) => { + return request.post('/api/account/updateTask', { taskId }); }; export const getAppConfig = () => { diff --git a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx index 53ba96f03dd..4d0d21927ad 100644 --- a/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/AuthModifyList.tsx @@ -1,12 +1,12 @@ import { useConfigStore } from '@/stores/config'; import useSessionStore, { OauthAction } from '@/stores/session'; import { OauthProvider } from '@/types/user'; -import { Text, Image, Center } from '@chakra-ui/react'; +import { Center, Image, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import router from 'next/router'; import { useMemo } from 'react'; -import { ConfigItem } from './ConfigItem'; import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; export function AuthModifyList({ isOnlyOne, @@ -34,17 +34,19 @@ export function AuthModifyList({ ({ url, provider, - clientId + clientId, + proxyAddress }: { url: string; provider: OauthProvider; clientId: string; + proxyAddress?: string; }) => (action: T) => { const state = generateState(action); setProvider(provider); - if (conf.proxyAddress) { - const target = new URL(conf.proxyAddress); + if (proxyAddress) { + const target = new URL(proxyAddress); const callback = new URL(conf.callbackURL); target.searchParams.append( 'oauthProxyState', @@ -69,6 +71,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'GITHUB', clientId: githubConf.clientID, + proxyAddress: githubConf?.proxyAddress, url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user` })(action); } @@ -83,6 +86,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'WECHAT', clientId: wechatConf.clientID, + proxyAddress: wechatConf?.proxyAddress, url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&scope=snsapi_login&#wechat_redirect` })(action); } @@ -99,6 +103,7 @@ export function AuthModifyList({ return actionCbGen({ provider: 'GOOGLE', clientId: googleConf.clientID, + proxyAddress: googleConf?.proxyAddress, url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&scope=${scope}&include_granted_scopes=true` })(action); } diff --git a/frontend/desktop/src/components/account/AccountCenter/index.tsx b/frontend/desktop/src/components/account/AccountCenter/index.tsx index fcc86cd3233..43a29c05a5e 100644 --- a/frontend/desktop/src/components/account/AccountCenter/index.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/index.tsx @@ -1,39 +1,39 @@ +import { UserInfo } from '@/api/auth'; +import PasswordModify from '@/components/account/AccountCenter/PasswordModify'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; +import { ValueOf } from '@/types'; import { + Badge, + Center, Flex, - Text, + HStack, + IconButton, + IconButtonProps, + Image, Modal, ModalBody, ModalCloseButton, ModalContent, - ModalOverlay, - useDisclosure, - IconButton, - IconButtonProps, ModalHeader, + ModalOverlay, Spinner, - Image, - HStack, - VStack, - Center, - Badge + Text, + useDisclosure, + VStack } from '@chakra-ui/react'; -import { useMemo, useState } from 'react'; +import { CloseIcon, LeftArrowIcon, SettingIcon } from '@sealos/ui'; import { useQuery } from '@tanstack/react-query'; -import useSessionStore from '@/stores/session'; import { useTranslation } from 'next-i18next'; -import { SettingIcon, LeftArrowIcon, CloseIcon } from '@sealos/ui'; -import { UserInfo } from '@/api/auth'; -import PasswordModify from '@/components/account/AccountCenter/PasswordModify'; -import { PhoneBind, EmailBind } from './SmsModify/SmsBind'; -import { PhoneUnBind, EmailUnBind } from './SmsModify/SmsUnbind'; -import { PhoneChange, EmailChange } from './SmsModify/SmsChange'; -import { BindingModifyButton, BINDING_STATE_MODIFY_BEHAVIOR } from './BindingModifyButton'; -import { ConfigItem } from './ConfigItem'; +import { useMemo, useState } from 'react'; +import { RealNameAuthForm } from '../RealNameModal'; import { AuthModifyList } from './AuthModifyList'; +import { BINDING_STATE_MODIFY_BEHAVIOR, BindingModifyButton } from './BindingModifyButton'; +import { ConfigItem } from './ConfigItem'; import DeleteAccount from './DeleteAccountModal'; -import { ValueOf } from '@/types'; -import { RealNameAuthForm } from '../RealNameModal'; -import { useConfigStore } from '@/stores/config'; +import { EmailBind, PhoneBind } from './SmsModify/SmsBind'; +import { EmailChange, PhoneChange } from './SmsModify/SmsChange'; +import { EmailUnBind, PhoneUnBind } from './SmsModify/SmsUnbind'; enum _PageState { INDEX = 0 // WECHAT_BIND, @@ -68,6 +68,7 @@ const PageState = Object.assign( export default function Index(props: Omit) { const { commonConfig } = useConfigStore(); const { session } = useSessionStore((s) => s); + const conf = useConfigStore(); const { t } = useTranslation(); const logo = '/images/default-user.svg'; const { isOpen, onOpen, onClose } = useDisclosure(); @@ -221,8 +222,18 @@ export default function Index(props: Omit) { {t('common:realname_info')}} RightElement={ + infoData.data.enterpriseVerificationStatus === 'Success' || infoData.data.realName ? ( - {infoData?.data.realName} + + + {infoData?.data.enterpriseRealName || infoData?.data.realName} + + ) : ( ) { } /> )} - {providerState.PASSWORD.isBinding && ( + {conf.authConfig?.idp.password.enabled && providerState.PASSWORD.isBinding && ( {t('common:password')}} RightElement={ @@ -252,40 +263,42 @@ export default function Index(props: Omit) { } /> )} - {t('common:phone')}} - RightElement={ - <> - - {providerState.PHONE.isBinding - ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') - : t('common:unbound')} - - - { - providerState.PHONE.isBinding - ? setPageState(PageState.PHONE_CHANGE_BIND) - : setPageState(PageState.PHONE_BIND); - }} - /> - {providerState.PHONE.isBinding && providerState.total > 1 && ( + {conf.authConfig?.idp.sms.enabled && ( + {t('common:phone')}} + RightElement={ + <> + + {providerState.PHONE.isBinding + ? providerState.PHONE.id.replace(/(\d{3})\d+(\d{4})/, '$1****$2') + : t('common:unbound')} + + { - setPageState(PageState.PHONE_UNBIND); + providerState.PHONE.isBinding + ? setPageState(PageState.PHONE_CHANGE_BIND) + : setPageState(PageState.PHONE_BIND); }} /> - )} - - - } - /> + {providerState.PHONE.isBinding && providerState.total > 1 && ( + { + setPageState(PageState.PHONE_UNBIND); + }} + /> + )} + + + } + /> + )} {t('common:email')}} RightElement={ diff --git a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx index 703e7e177de..0bb7c6c6bf6 100644 --- a/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx +++ b/frontend/desktop/src/components/account/AccountCenter/mergeUser/NeedToMergeModal.tsx @@ -1,27 +1,27 @@ +import { mergeUserRequest } from '@/api/auth'; import { useCustomToast } from '@/hooks/useCustomToast'; +import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; +import { ValueOf } from '@/types'; +import { I18nErrorKey } from '@/types/i18next'; +import { USER_MERGE_STATUS } from '@/types/response/merge'; import { - Text, + BoxProps, Button, + HStack, Modal, - ModalOverlay, - ModalContent, + ModalBody, ModalCloseButton, + ModalContent, ModalHeader, + ModalOverlay, Spinner, - ModalBody, - BoxProps, - VStack, - HStack + Text, + VStack } from '@chakra-ui/react'; import { WarnTriangeIcon } from '@sealos/ui'; -import { useQueryClient, useMutation } from '@tanstack/react-query'; +import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'next-i18next'; -import { mergeUserRequest } from '@/api/auth'; -import useCallbackStore, { MergeUserStatus } from '@/stores/callback'; import { useEffect, useState } from 'react'; -import { USER_MERGE_STATUS } from '@/types/response/merge'; -import { ValueOf } from '@/types'; -import { I18nErrorKey } from '@/types/i18next'; function NeedToMerge({ ...props }: BoxProps & {}) { const { mergeUserStatus, mergeUserData, setMergeUserStatus, setMergeUserData } = @@ -63,20 +63,22 @@ function NeedToMerge({ ...props }: BoxProps & {}) { - + {t('common:merge_account_title')} @@ -84,7 +86,7 @@ function NeedToMerge({ ...props }: BoxProps & {}) { {mutation.isLoading ? ( ) : ( - + {mergeUserStatus === MergeUserStatus.CONFLICT diff --git a/frontend/desktop/src/components/account/RealNameModal.tsx b/frontend/desktop/src/components/account/RealNameModal.tsx index 18d5531209a..31f591fdaef 100644 --- a/frontend/desktop/src/components/account/RealNameModal.tsx +++ b/frontend/desktop/src/components/account/RealNameModal.tsx @@ -9,40 +9,40 @@ import { useDisclosure, VStack, FormControl, - HStack, FormLabel, Button, - FormErrorMessage, Input, useToast, - FlexProps, UseToastOptions, Box, Flex, Center, Spinner, - InputGroup, - InputRightElement, - Link + Link, + FormErrorMessage, + HStack, + TabIndicator, + FlexProps } from '@chakra-ui/react'; import { CloseIcon, useMessage, WarningIcon } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import React, { MouseEventHandler, ReactElement, useEffect, useState } from 'react'; +import React, { ReactElement, useCallback, useEffect, useState } from 'react'; +import { Tabs, TabList, TabPanels, Tab, TabPanel } from '@chakra-ui/react'; import { useForm } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; -import { identityCodeValid } from '@/utils/tools'; import { - getSmsBindCodeRequest, - realNameAuthRequest, - UserInfo, - verifySmsBindRequest + enterpriseRealNameAuthRequest, + faceAuthGenerateQRcodeUriRequest, + getFaceAuthStatusRequest } from '@/api/auth'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import useSessionStore from '@/stores/session'; import { useQuery } from '@tanstack/react-query'; -import { useTimer } from '@/hooks/useTimer'; -import { SmsType } from '@/services/backend/db/verifyCode'; +import QRCode from 'qrcode.react'; +import { useDropzone } from 'react-dropzone'; +import { useConfigStore } from '@/stores/config'; +import { DeleteIcon, PictureIcon, UploadIcon, AttachmentIcon } from '../icons'; export function useRealNameAuthNotification(props?: UseToastOptions) { const { t } = useTranslation(); @@ -136,49 +136,538 @@ export function useRealNameAuthNotification(props?: UseToastOptions) { }; } +function RealNameModal(props: { + children: React.ReactElement; + onModalOpen?: () => void; + onModalClose?: () => void; + onFormSuccess?: () => void; +}): ReactElement { + const { t } = useTranslation(); + const { children } = props; + const { isOpen, onOpen, onClose } = useDisclosure(); + + const handleClose = () => { + onClose(); + if (props.onModalClose && typeof props.onModalClose === 'function') { + props.onModalClose(); + } + }; + + return ( + <> + {children && + React.cloneElement(children, { + onClick: () => { + onOpen(); + if (props.onModalOpen && typeof props.onModalOpen === 'function') { + props.onModalOpen(); + } + } + })} + + + + + + + {t('common:realName_verification')} + + + + + + {t('common:personal_verification')} + + {/* + {t('common:enterprise_verification')} + */} + + + + + { + onClose(); + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + {/* + { + onClose(); + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + */} + + + + + + + ); +} + export function RealNameAuthForm( props: FlexProps & { onFormSuccess?: () => void; } -): ReactElement { - const { message } = useMessage(); +) { const { t } = useTranslation(); - const { setSessionProp } = useSessionStore(); - const { session } = useSessionStore((s) => s); - const [phoneNumber, setPhoneNumber] = useState(null); + return ( + + + + {t('common:personal_verification')} + + {/* + {t('common:enterprise_verification')} + */} + + + + + { + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + + {/* + { + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + }} + /> + */} + + + ); +} +export function FaceIdRealNameAuthORcode( + props: FlexProps & { + onFormSuccess?: () => void; + } +) { + const { t } = useTranslation(); + const { message } = useMessage(); const queryClient = useQueryClient(); - const infoData = useQuery({ - queryFn: UserInfo, - queryKey: [session?.token, 'UserInfo'], - select(d) { - return d.data?.info; + const [isPolling, setIsPolling] = useState(false); + const { session } = useSessionStore((s) => s); + const { setSessionProp } = useSessionStore(); + const [refetchCount, setRefetchCount] = useState(0); + + const { data, isLoading, error, refetch } = useQuery( + ['faceIdAuth'], + faceAuthGenerateQRcodeUriRequest, + { + retry: false, + refetchOnWindowFocus: false } - }); + ); + + const handleRefetch = useCallback(() => { + setRefetchCount((prev) => prev + 1); + refetch(); + }, [refetch]); + + useEffect(() => { + let intervalId: NodeJS.Timeout; + + const bizToken = data?.data?.bizToken; + + if (!bizToken) { + return; + } + + const stopPolling = () => { + if (intervalId) clearInterval(intervalId); + setIsPolling(false); + }; + + const startPolling = () => { + if (!isPolling) { + setIsPolling(true); + intervalId = setInterval(async () => { + try { + const result = await getFaceAuthStatusRequest({ bizToken }); + if (result.data?.status === 'Success') { + message({ + title: t('common:face_recognition_success'), + status: 'success', + duration: 2000, + isClosable: true + }); + + setSessionProp('user', { + ...useSessionStore.getState().session!.user!, + realName: result.data?.realName + }); + + queryClient.invalidateQueries([session?.token, 'UserInfo']); + + stopPolling(); + + if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { + props.onFormSuccess(); + } + } + if (result.data?.status === 'Failed') { + message({ + title: t('common:face_recognition_failed'), + status: 'error', + duration: 2000, + isClosable: true + }); + + stopPolling(); + handleRefetch(); + } + } catch (error: any) { + console.error('Error checking face ID auth status:', error); + message({ + title: error.message, + status: 'error', + duration: 2000, + isClosable: true + }); + } + }, 2000); + } + }; + + startPolling(); + + return stopPolling; + }, [session?.token, data, data?.data?.bizToken, refetchCount]); + + if (error) { + return ( + + {t('common:failed_to_get_qr_code')} + + + + + ); + } + + if (isLoading) { + return ( +
+ + {t('common:loading')} +
+ ); + } + + return ( + + {data?.data?.url && ( + <> + + + {t('common:scan_qr_code_for_face_recognition')} + +
+ +
+ {isPolling && ( + + {t('common:waiting_for_face_recognition')} + + )} +
+ + )} +
+ ); +} - const { seconds, startTimer, isRunning } = useTimer({ - duration: 60, - step: 1 +function FileUploadBox({ + onDrop, + file, + removeFile, + label, + description, + isAttachment +}: { + onDrop: (acceptedFiles: File[]) => void; + file: File | null; + removeFile: () => void; + label: string; + description: string; + isAttachment?: boolean; +}) { + const { commonConfig } = useConfigStore((s) => s); + const enterpriseSupportingMaterialsUri = commonConfig?.enterpriseSupportingMaterials; + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + maxFiles: 1, + accept: { + 'image/*': ['.png', '.jpg', '.jpeg'], + 'application/pdf': ['.pdf'] + } }); - const remainTime = 60 - seconds; + const { t } = useTranslation(); + + return ( + + + {label} + + + + {description} + + + + + + + {t('common:click_to_upload_file')} + + + {' '} + + + {file ? ( + + + + + {file.name} + + + + + ) : null} + + {isAttachment && ( + + + + + {t('common:attachment')} + + + + )} + + + + ); +} + +function EnterpriseVerification( + props: FlexProps & { + onFormSuccess?: () => void; + } +) { + const { t } = useTranslation(); + const { message } = useMessage(); + + const { session } = useSessionStore((s) => s); + const { setSessionProp } = useSessionStore(); + + const queryClient = useQueryClient(); const schema = z.object({ - name: z - .string() - .min(1, { message: t('common:name_required') }) - .max(20, { message: t('common:name_required') }), - phone: z + enterpriseName: z .string() - .min(1, { message: t('common:phone_invalid') }) - .regex(/^\d+$/, { message: t('common:phone_invalid') }) - .max(16, { message: t('common:phone_invalid') }), - idCard: z.string().refine(identityCodeValid, { message: t('common:idCard_invalid') }), - verifyCode: z - .string() - .optional() - .refine((val) => (!phoneNumber ? /^\d{6}$/.test(val || '') : true), { - message: t('common:verifyCode_invalid') - }) + .min(1, { message: t('common:enterprise_name_required') }) + .max(50, { message: t('common:enterprise_name_required') }), + enterpriseQualification: z.instanceof(File).nullable(), + supportingMaterials: z.instanceof(File).nullable() }); type FormData = z.infer; @@ -186,106 +675,58 @@ export function RealNameAuthForm( const { register, handleSubmit, - reset, - trigger, - getValues, setValue, + watch, + reset, formState: { errors } } = useForm({ resolver: zodResolver(schema), - mode: 'onChange' + defaultValues: { + enterpriseName: '', + enterpriseQualification: null, + supportingMaterials: null + } }); - useEffect(() => { - if (infoData.isSuccess && infoData.data) { - const phoneProvider = infoData.data.oauthProvider.find((p) => p.providerType === 'PHONE'); - const phoneNumber = phoneProvider?.providerId || null; - setPhoneNumber(phoneNumber); - if (phoneNumber) { - setValue('phone', phoneNumber, { shouldValidate: true }); - } - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [infoData.isSuccess, infoData.data]); + const enterpriseQualification = watch('enterpriseQualification'); + const supportingMaterials = watch('supportingMaterials'); - const getCodeMutation = useMutation({ - mutationFn({ id, smsType }: { id: string; smsType: SmsType }) { - return getSmsBindCodeRequest(smsType)({ id }); - }, - onSuccess(data) { - startTimer(); - message({ - status: 'success', - title: t('common:already_sent_code'), - position: 'top', - duration: 2000, - isClosable: true - }); + const onDropEnterpriseQualification = useCallback( + (acceptedFiles: File[]) => { + setValue('enterpriseQualification', acceptedFiles[0], { shouldValidate: true }); }, - onError(err) { - getCodeMutation.reset(); - message({ - status: 'error', - title: t('common:get_code_failed'), - position: 'top', - duration: 2000, - isClosable: true - }); - } - }); + [setValue] + ); - const getCode: MouseEventHandler = async (e) => { - e.preventDefault(); - if (isRunning) { - message({ - status: 'warning', - title: t('common:already_sent_code'), - position: 'top', - duration: 2000, - isClosable: true - }); - return; - } - if (!(await trigger('phone'))) { - message({ - status: 'error', - title: t('common:invalid_phone_number'), - position: 'top', - duration: 2000, - isClosable: true - }); - return; - } - const id = getValues('phone'); - getCodeMutation.mutate({ - id, - smsType: 'phone' - }); - }; + const onDropCertificateMaterial = useCallback( + (acceptedFiles: File[]) => { + setValue('supportingMaterials', acceptedFiles[0], { shouldValidate: true }); + }, + [setValue] + ); - const verifyCodeAndSmsBindMutation = useMutation({ - async mutationFn({ smsType, ...data }: { id: string; code: string; smsType: SmsType }) { - return verifySmsBindRequest('phone')(data); - } - }); + const removeEnterpriseQualification = () => + setValue('enterpriseQualification', null, { shouldValidate: true }); + const removeSupportingMaterials = () => + setValue('supportingMaterials', null, { shouldValidate: true }); - const realNameAuthMutation = useMutation(realNameAuthRequest, { + const enterpriseRealNameAuthMutation = useMutation(enterpriseRealNameAuthRequest, { onSuccess: (data) => { if (data.code === 200) { message({ - title: t('common:realname_auth_success'), + title: t('common:upload_success'), status: 'success', duration: 2000, - isClosable: true + isClosable: true, + position: 'top' }); + queryClient.invalidateQueries([session?.token, 'UserInfo']); setSessionProp('user', { ...useSessionStore.getState().session!.user!, - realName: data.data?.name + enterpriseVerificationStatus: data.data?.status }); - queryClient.invalidateQueries([session?.token, 'UserInfo']); - reset(); if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { @@ -312,42 +753,27 @@ export function RealNameAuthForm( } }); - const onValidate = async (data: FormData) => { - if (!phoneNumber) { - try { - const verifyResult = await verifyCodeAndSmsBindMutation.mutateAsync({ - id: data.phone, - code: data.verifyCode!, - smsType: 'phone' - }); - - if (verifyResult.code !== 200) { - message({ - title: t('common:realname_auth_failed_tips'), - status: 'error', - position: 'top', - duration: 2000, - isClosable: true - }); + const onValidate = async (data: { + enterpriseName: string; + enterpriseQualification: File | null; + supportingMaterials: File | null; + }) => { + if (!data.enterpriseQualification || !data.supportingMaterials) { + message({ + title: t('common:please_fill_all_fields'), + status: 'warning', + position: 'top', + duration: 2000, + isClosable: true + }); - return; - } - } catch (error) { - message({ - title: (error as Error).message, - status: 'error', - position: 'top', - duration: 2000, - isClosable: true - }); - return; - } + return; } - - realNameAuthMutation.mutate({ - name: data.name, - idCard: data.idCard - }); + const formData = new FormData(); + formData.append('enterpriseName', data.enterpriseName); + formData.append('enterpriseQualification', data.enterpriseQualification); + formData.append('supportingMaterials', data.supportingMaterials); + enterpriseRealNameAuthMutation.mutate(formData); }; const onInvalid = () => { @@ -366,226 +792,91 @@ export function RealNameAuthForm( const onSubmit = handleSubmit(onValidate, onInvalid); return ( - <> - {infoData.isSuccess && infoData.data ? ( - - {/* Notification area */} - - {t('common:realname_auth_tips_a')} - {t('common:realname_auth_tips_b')} - - - {/* Form area */} -
- - - - - {t('common:name')} - - - - {errors.name && {errors.name.message}} - - - - - - {t('common:phone')} - - - - {!phoneNumber && ( - - - {t('common:get_code')} - - - )} - - - {errors.phone && {errors.phone.message}} - - - {!phoneNumber && ( - - - - {t('common:verifycode')} - - - - - {isRunning && {remainTime} s} - - - - {errors.verifyCode && ( - {errors.verifyCode.message} - )} - - )} - - - - - {t('common:idCard')} - - - - {errors.idCard && {errors.idCard.message}} - - - - -
-
- ) : ( -
- -
- )} - - ); -} - -function RealNameModal(props: { - children: React.ReactElement; - onModalOpen?: () => void; - onModalClose?: () => void; - onFormSuccess?: () => void; -}): ReactElement { - const { t } = useTranslation(); - const { children } = props; - const { isOpen, onOpen, onClose } = useDisclosure(); - - const handleClose = () => { - onClose(); - if (props.onModalClose && typeof props.onModalClose === 'function') { - props.onModalClose(); - } - }; - - return ( - <> - {children && - React.cloneElement(children, { - onClick: () => { - onOpen(); - if (props.onModalOpen && typeof props.onModalOpen === 'function') { - props.onModalOpen(); - } - } - })} - - - - - - + + + - {t('common:realName_verification')} - - - { - onClose(); - if (props.onFormSuccess && typeof props.onFormSuccess === 'function') { - props.onFormSuccess(); - } - }} + + {t('common:enterprise_name')} + + - - - - + + {errors.enterpriseName && ( + {errors.enterpriseName.message} + )} + + + + + + + + + + + +
+ ); } + export default RealNameModal; diff --git a/frontend/desktop/src/components/account/index.tsx b/frontend/desktop/src/components/account/index.tsx index 39c3052913d..fa907229490 100644 --- a/frontend/desktop/src/components/account/index.tsx +++ b/frontend/desktop/src/components/account/index.tsx @@ -144,7 +144,7 @@ export default function Account() {
showDisclosure.onOpen()}> - +
diff --git a/frontend/desktop/src/components/desktop_content/apps.tsx b/frontend/desktop/src/components/desktop_content/apps.tsx index 8e6db4630af..084c5ec4cee 100644 --- a/frontend/desktop/src/components/desktop_content/apps.tsx +++ b/frontend/desktop/src/components/desktop_content/apps.tsx @@ -109,6 +109,7 @@ export default function Apps() { gap={`${gridSpacing}px`} templateColumns={`repeat(auto-fill, minmax(${appWidth}px, 1fr))`} templateRows={`repeat(auto-fit, ${appHeight}px)`} + className="apps-container" > {paginatedApps && paginatedApps.map((item: TApp, index) => ( @@ -121,9 +122,9 @@ export default function Apps() { userSelect="none" cursor={'pointer'} onClick={(e) => handleDoubleClick(e, item)} + className={item.key} > import('../AppDock'), { ssr: false }); const FloatButton = dynamic(() => import('@/components/floating_button'), { ssr: false }); @@ -51,6 +52,11 @@ export default function Desktop(props: any) { const { session } = useSessionStore(); const { commonConfig } = useConfigStore(); const realNameAuthNotificationIdRef = useRef(); + const [isClient, setIsClient] = useState(false); + + useEffect(() => { + setIsClient(true); + }, []); const infoData = useQuery({ queryFn: UserInfo, @@ -105,6 +111,9 @@ export default function Desktop(props: any) { [apps, openApp, runningInfo, setToHighestLayerById] ); + const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); + const { UserGuide, tasks, desktopGuide, handleCloseTaskModal } = useDriver(); + useEffect(() => { const cleanup = createMasterAPP(); return cleanup; @@ -116,11 +125,13 @@ export default function Desktop(props: any) { }, [openDesktopApp]); useEffect(() => { - if (infoData.isSuccess && !infoData?.data?.realName && commonConfig?.realNameAuthEnabled) { - realNameAuthNotificationIdRef.current = realNameAuthNotification({ - duration: null, - isClosable: true - }); + if (infoData.isSuccess && commonConfig?.realNameAuthEnabled) { + if (!infoData?.data?.realName && infoData?.data?.enterpriseVerificationStatus !== 'Success') { + realNameAuthNotificationIdRef.current = realNameAuthNotification({ + duration: null, + isClosable: true + }); + } } return () => { @@ -234,22 +245,70 @@ export default function Desktop(props: any) { - {/* {showGuide ? ( - <> - - - - ) : ( - <> - )} */} + {isClient && ( + + {desktopGuide && ( + <> + + + + )} + {taskComponentState === 'modal' && tasks?.length > 0 && ( + { + switch (task.taskType) { + case 'LAUNCHPAD': + openDesktopApp({ + appKey: 'system-applaunchpad', + pathname: '/app/edit', + messageData: { + type: 'InternalAppCall' + } + }); + break; + case 'DATABASE': + openDesktopApp({ + appKey: 'system-dbprovider', + pathname: '/db/edit', + messageData: { type: 'InternalAppCall' } + }); + break; + case 'APPSTORE': + openDesktopApp({ + appKey: 'system-template', + pathname: '/', + messageData: { + type: 'InternalAppCall' + } + }); + break; + default: + console.log(task.taskType); + } + setTaskComponentState('button'); + }} + /> + )} + {taskComponentState === 'button' && ( + { + setTaskComponentState('modal'); + }} + /> + )} + + )} {isAppBar ? : } diff --git a/frontend/desktop/src/components/icons/index.tsx b/frontend/desktop/src/components/icons/index.tsx index 96e183625c8..ca179965342 100644 --- a/frontend/desktop/src/components/icons/index.tsx +++ b/frontend/desktop/src/components/icons/index.tsx @@ -383,3 +383,264 @@ export function HelpIcon(props: IconProps) { ); } + +export function LaunchpadIcon(props: IconProps) { + return ( + + + + + + + ); +} + +export function DBproviderIcon(props: IconProps) { + return ( + + + + + + ); +} + +export function AppStoreIcon(props: IconProps) { + return ( + + + + ); +} + +export function DriverStarIcon() { + return ( + + + + + + + + + + + ); +} + +export function IdeaIcon(props: IconProps) { + return ( + + + + + + + + + + + + + + + + + + ); +} + +export function RightArrowIcon(props: IconProps) { + return ( + + + + ); +} + +export function UploadIcon(props: IconProps) { + return ( + + + + ); +} + +export function PictureIcon(props: IconProps) { + return ( + + + + + ); +} + +export function DeleteIcon(props: IconProps) { + return ( + + + + ); +} + +export function AttachmentIcon(props: IconProps) { + return ( + + + + ); +} diff --git a/frontend/desktop/src/components/notification/index.tsx b/frontend/desktop/src/components/notification/index.tsx index b06b1b2be7d..0d0f704bcc8 100644 --- a/frontend/desktop/src/components/notification/index.tsx +++ b/frontend/desktop/src/components/notification/index.tsx @@ -3,7 +3,7 @@ import request from '@/services/request'; import useAppStore from '@/stores/app'; import { formatTime } from '@/utils/tools'; import { Box, Button, Flex, Text, UseDisclosureReturn } from '@chakra-ui/react'; -import { ClearOutlineIcon, CloseIcon, WarnIcon, useMessage } from '@sealos/ui'; +import { ClearOutlineIcon, CloseIcon, NotificationIcon, WarnIcon, useMessage } from '@sealos/ui'; import { useMutation, useQuery } from '@tanstack/react-query'; import clsx from 'clsx'; import { produce } from 'immer'; @@ -143,6 +143,17 @@ export default function Notification(props: NotificationProps) { } }, [i18n.language, refetch]); + const getNotificationIcon = (from: string | undefined) => { + switch (from) { + case 'Debt-System': + return ; + case 'Active-System': + return '🎉'; + default: + return ; + } + }; + return disclosure.isOpen ? ( <> @@ -316,7 +327,7 @@ export default function Notification(props: NotificationProps) { color={'white'} > - + {getNotificationIcon(MessageConfig.popupMessage?.i18n['en']?.from)} {MessageConfig.popupMessage?.i18n[i18n.language]?.title} diff --git a/frontend/desktop/src/components/signin/auth/AuthList.tsx b/frontend/desktop/src/components/signin/auth/AuthList.tsx index 6f9b7edb746..d353af996da 100644 --- a/frontend/desktop/src/components/signin/auth/AuthList.tsx +++ b/frontend/desktop/src/components/signin/auth/AuthList.tsx @@ -1,152 +1,204 @@ import { useConfigStore } from '@/stores/config'; import useSessionStore from '@/stores/session'; import { OauthProvider } from '@/types/user'; -import { Button, Image, Flex, Icon, Center } from '@chakra-ui/react'; +import { Button, Center, Flex, FlexProps, Icon, Image, Text } from '@chakra-ui/react'; import { GithubIcon, GoogleIcon, WechatIcon } from '@sealos/ui'; +import { useTranslation } from 'next-i18next'; import { useRouter } from 'next/router'; import { MouseEventHandler, useMemo } from 'react'; -const AuthList = () => { +const AuthList = ({ + zeroTab = false, + isAgreeCb, + ...props +}: { zeroTab?: boolean; isAgreeCb: () => boolean } & FlexProps) => { const conf = useConfigStore().authConfig; + const { t } = useTranslation(['common']); const router = useRouter(); const logo = useConfigStore().layoutConfig?.logo; const { generateState, setProvider } = useSessionStore(); - const authList: { icon: typeof Icon; cb: MouseEventHandler; need: boolean }[] = useMemo(() => { - if (!conf) return []; - const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { - setProvider(provider); - window.location.href = url; - }; - const oauthProxyLogin = async ({ - state, - provider, - id - }: { - state: string; - provider: OauthProvider; - id: string; - }) => { - // if(!conf) return - setProvider(provider); - const target = new URL(conf.proxyAddress); - const callback = new URL(conf.callbackURL); - target.searchParams.append( - 'oauthProxyState', - encodeURIComponent(callback.toString()) + '_' + state - ); - target.searchParams.append('oauthProxyClientID', id); - target.searchParams.append('oauthProxyProvider', provider); - router.replace(target.toString()); - }; - return [ - { - icon: GithubIcon, - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const githubConf = conf?.idp.github; - if (conf?.proxyAddress) - oauthProxyLogin({ - provider: 'GITHUB', - state, - id: githubConf?.clientID as string - }); - else - oauthLogin({ - provider: 'GITHUB', - url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user&state=${state}` - }); + const authList: { icon: typeof Icon; cb: MouseEventHandler; need: boolean; text: string }[] = + useMemo(() => { + if (!conf) return []; + const oauthLogin = async ({ url, provider }: { url: string; provider?: OauthProvider }) => { + setProvider(provider); + window.location.href = url; + }; + const oauthProxyLogin = async ({ + state, + provider, + proxyAddress, + id + }: { + state: string; + proxyAddress: string; + provider: OauthProvider; + id: string; + }) => { + // if(!conf) return + setProvider(provider); + const target = new URL(proxyAddress); + const callback = new URL(conf.callbackURL); + target.searchParams.append( + 'oauthProxyState', + encodeURIComponent(callback.toString()) + '_' + state + ); + target.searchParams.append('oauthProxyClientID', id); + target.searchParams.append('oauthProxyProvider', provider); + router.replace(target.toString()); + }; + return [ + { + icon: GithubIcon, + cb: (e) => { + e.preventDefault(); + if (!isAgreeCb()) return; + const state = generateState(); + const githubConf = conf?.idp.github; + if (githubConf.proxyAddress) + oauthProxyLogin({ + provider: 'GITHUB', + state, + proxyAddress: githubConf.proxyAddress, + id: githubConf?.clientID as string + }); + else + oauthLogin({ + provider: 'GITHUB', + url: `https://github.com/login/oauth/authorize?client_id=${githubConf?.clientID}&redirect_uri=${conf?.callbackURL}&scope=user:email%20read:user&state=${state}` + }); + }, + text: t('login_with_github'), + need: conf.idp.github.enabled }, - need: conf.idp.github.enabled - }, - { - icon: WechatIcon, - cb: (e) => { - const wechatConf = conf?.idp.wechat; - e.preventDefault(); - const state = generateState(); - if (conf.proxyAddress) - oauthProxyLogin({ - provider: 'WECHAT', - state, - id: conf.idp.wechat?.clientID - }); - else - oauthLogin({ - provider: 'WECHAT', - url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&state=${state}&scope=snsapi_login&#wechat_redirect` - }); + { + icon: WechatIcon, + cb: (e) => { + if (!isAgreeCb()) return; + const wechatConf = conf?.idp.wechat; + e.preventDefault(); + const state = generateState(); + if (wechatConf.proxyAddress) + oauthProxyLogin({ + provider: 'WECHAT', + state, + proxyAddress: wechatConf.proxyAddress, + id: conf.idp.wechat?.clientID + }); + else + oauthLogin({ + provider: 'WECHAT', + url: `https://open.weixin.qq.com/connect/qrconnect?appid=${wechatConf?.clientID}&redirect_uri=${conf?.callbackURL}&response_type=code&state=${state}&scope=snsapi_login&#wechat_redirect` + }); + }, + text: t('login_with_wechat'), + need: conf?.idp.wechat?.enabled as boolean }, - need: conf?.idp.wechat?.enabled as boolean - }, - { - icon: GoogleIcon, - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const googleConf = conf?.idp.google; - const scope = encodeURIComponent( - `https://www.googleapis.com/auth/userinfo.profile openid` - ); - if (conf.proxyAddress) - oauthProxyLogin({ - state, - provider: 'GOOGLE', - id: googleConf.clientID - }); - else - oauthLogin({ - provider: 'GOOGLE', - url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true` - }); + { + icon: GoogleIcon, + cb: (e) => { + e.preventDefault(); + if (!isAgreeCb()) return; + const state = generateState(); + const googleConf = conf?.idp.google; + const scope = encodeURIComponent( + `https://www.googleapis.com/auth/userinfo.profile openid` + ); + if (googleConf.proxyAddress) + oauthProxyLogin({ + state, + provider: 'GOOGLE', + proxyAddress: googleConf.proxyAddress, + id: googleConf.clientID + }); + else + oauthLogin({ + provider: 'GOOGLE', + url: `https://accounts.google.com/o/oauth2/v2/auth?client_id=${googleConf.clientID}&redirect_uri=${conf.callbackURL}&response_type=code&state=${state}&scope=${scope}&include_granted_scopes=true` + }); + }, + text: t('login_with_google'), + need: conf.idp.google.enabled as boolean }, - need: conf.idp.google.enabled as boolean - }, - { - icon: () => ( -
- logo -
- ), - cb: (e) => { - e.preventDefault(); - const state = generateState(); - const oauth2Conf = conf?.idp.oauth2; - if (conf.proxyAddress) - oauthProxyLogin({ - provider: 'OAUTH2', - state, - id: oauth2Conf.clientID as string - }); - else - oauthLogin({ - provider: 'OAUTH2', - url: `${oauth2Conf?.authURL}?client_id=${oauth2Conf.clientID}&redirect_uri=${oauth2Conf.callbackURL}&response_type=code&state=${state}` - }); - }, - need: conf.idp.oauth2?.enabled as boolean - } - ]; - }, [conf, logo, router]); + { + icon: () => ( +
+ logo +
+ ), + cb: (e) => { + e.preventDefault(); + const state = generateState(); + const oauth2Conf = conf?.idp.oauth2; + if (oauth2Conf.proxyAddress) + oauthProxyLogin({ + provider: 'OAUTH2', + state, + proxyAddress: oauth2Conf.proxyAddress, + id: oauth2Conf.clientID as string + }); + else + oauthLogin({ + provider: 'OAUTH2', + url: `${oauth2Conf?.authURL}?client_id=${oauth2Conf.clientID}&redirect_uri=${oauth2Conf.callbackURL}&response_type=code&state=${state}` + }); + }, + text: t('login_with_oauth2'), + need: conf.idp.oauth2?.enabled as boolean + } + ]; + }, [conf, logo, router, isAgreeCb]); return ( - + {authList .filter((item) => item.need) .map((item, index) => ( ))} diff --git a/frontend/desktop/src/components/signin/auth/usePassword.tsx b/frontend/desktop/src/components/signin/auth/usePassword.tsx index 1a33b6e718b..be8ed7cecf7 100644 --- a/frontend/desktop/src/components/signin/auth/usePassword.tsx +++ b/frontend/desktop/src/components/signin/auth/usePassword.tsx @@ -84,7 +84,7 @@ export default function usePassword({ const infoData = await UserInfo(); const payload = jwtDecode(regionResult.data.token); setSession({ - token: regionResult.data.token, + token: regionResult.data.appToken, // fix cannot get appToken after login user: { k8s_username: payload.userCrName, name: infoData.data?.info.nickname || '', @@ -95,6 +95,9 @@ export default function usePassword({ userId: payload.userId, userUid: payload.userUid, realName: infoData.data?.info.realName || undefined, + enterpriseVerificationStatus: + infoData.data?.info.enterpriseVerificationStatus || undefined, + enterpriseRealName: infoData.data?.info.enterpriseRealName || undefined, userRestrictedLevel: infoData.data?.info.userRestrictedLevel || undefined }, kubeconfig: regionResult.data.kubeconfig diff --git a/frontend/desktop/src/components/signin/auth/useProtocol.tsx b/frontend/desktop/src/components/signin/auth/useProtocol.tsx index c381ee8cfdd..e8ba66c3e5b 100644 --- a/frontend/desktop/src/components/signin/auth/useProtocol.tsx +++ b/frontend/desktop/src/components/signin/auth/useProtocol.tsx @@ -1,4 +1,4 @@ -import { Checkbox, Flex, Link, Text, TextProps } from '@chakra-ui/react'; +import { Checkbox, Flex, Link, Text } from '@chakra-ui/react'; import { useTranslation } from 'next-i18next'; import { useState } from 'react'; @@ -12,7 +12,6 @@ const useProtocol = ({ const { t, i18n } = useTranslation(); const [isAgree, setIsAgree] = useState(false); const [isInvalid, setIsInvalid] = useState(false); - const Protocol = () => ( (LoginType.NONE); @@ -119,15 +121,19 @@ export default function SigninComponent() { [loginConfig, tabIndex] ); - const handleLogin = debounce(() => { - const selectedConfig = loginConfig[tabIndex]; - if (isAgree && selectedConfig) { - const { login } = selectedConfig; - login(); + const isAgreeCb = () => { + if (isAgree) { + return true; } else { setIsInvalid(true); showError(t('common:read_and_agree')); + return false; } + }; + const handleLogin = debounce(() => { + const selectedConfig = loginConfig[tabIndex]; + if (!isAgreeCb() || !selectedConfig) return; + selectedConfig.login(); }, 500); return ( {conf.layoutConfig?.meta.title || ''} + - - - {conf.layoutConfig?.title} - - + {needTabsCount > 0 && ( + + + {conf.layoutConfig?.title} + + + )} - {pageState === 0 && needTabs && ( + {pageState === 0 && needTabsCount > 1 && ( { @@ -205,45 +214,60 @@ export default function SigninComponent() { )} {LoginComponent} - - {tabIndex !== LoginType.WeChat && ( - <> - - {!!conf.commonConfig?.cfSiteKey && ( - 0 ? ( + <> + + {!!conf.commonConfig?.cfSiteKey && ( + + )} + - - - )} + width="266px" + minH="42px" + mb="14px" + borderRadius="4px" + p="10px" + onClick={handleLogin} + > + {isLoading + ? (t('common:loading') || 'Loading') + '...' + : t('common:log_in') || 'Log In'} + + + + ) : ( + <> + + {conf.layoutConfig?.title} + + + + + ))} diff --git a/frontend/desktop/src/components/task/floatButton.tsx b/frontend/desktop/src/components/task/floatButton.tsx new file mode 100644 index 00000000000..4a979ee2922 --- /dev/null +++ b/frontend/desktop/src/components/task/floatButton.tsx @@ -0,0 +1,44 @@ +import { Flex, Text } from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React from 'react'; +import { IdeaIcon } from '../icons'; + +interface FloatingTaskButtonProps { + onClick: () => void; +} + +const FloatingTaskButton: React.FC = ({ onClick }) => { + const { t } = useTranslation(); + + return ( + + + + {t('common:newuser_benefit')} + + + ); +}; + +export default FloatingTaskButton; diff --git a/frontend/desktop/src/components/task/taskModal.tsx b/frontend/desktop/src/components/task/taskModal.tsx new file mode 100644 index 00000000000..b78c95413c4 --- /dev/null +++ b/frontend/desktop/src/components/task/taskModal.tsx @@ -0,0 +1,157 @@ +import { IdeaIcon, RightArrowIcon } from '@/components/icons'; +import { I18nCommonKey } from '@/types/i18next'; +import { UserTask } from '@/types/task'; +import { formatMoney } from '@/utils/format'; +import { + Box, + Button, + Flex, + HStack, + Image, + Modal, + ModalContent, + ModalOverlay, + Text, + VStack +} from '@chakra-ui/react'; +import { useTranslation } from 'next-i18next'; +import React from 'react'; + +interface TaskModalProps { + isOpen: boolean; + onClose: () => void; + tasks: UserTask[]; + onTaskClick: (task: UserTask) => void; +} + +const TaskModal: React.FC = ({ isOpen, onClose, tasks, onTaskClick }) => { + const { t, i18n } = useTranslation(); + + const boxStyles = { + border: '1px solid rgba(60, 101, 172, 0.08)', + borderRadius: '6px', + padding: '16px 20px', + backgroundColor: '#FFFFFF', + width: '100%', + cursor: 'pointer' + }; + + return ( + + + + background + + + + {t('common:sealos_newcomer_benefits')} + + + {tasks.map((task) => ( + onTaskClick(task)}> + + + + + {task?.title?.[i18n.language]} + + + + + {t('common:balance')} +{formatMoney(Number(task.reward) || 0)} + + + {task.isCompleted ? ( + + {t('common:completed')} + + ) : ( + + + {t('common:start_now')} + + + + )} + + + ))} + + + + + + + + + + + ); +}; + +export default TaskModal; diff --git a/frontend/desktop/src/components/task/useDriver.tsx b/frontend/desktop/src/components/task/useDriver.tsx new file mode 100644 index 00000000000..dcdbe41d02c --- /dev/null +++ b/frontend/desktop/src/components/task/useDriver.tsx @@ -0,0 +1,349 @@ +import { checkUserTask, getUserTasks, updateTask } from '@/api/platform'; +import { AppStoreIcon, DBproviderIcon, DriverStarIcon, LaunchpadIcon } from '@/components/icons'; +import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; +import { UserTask } from '@/types/task'; +import { Box, Button, Flex, FlexProps, Icon, Image, Text, useMediaQuery } from '@chakra-ui/react'; +import { driver } from '@sealos/driver'; +import { useTranslation } from 'next-i18next'; +import { useEffect, useState } from 'react'; + +export default function useDriver() { + const { t } = useTranslation(); + const [desktopGuide, setDesktopGuide] = useState(false); + const { layoutConfig } = useConfigStore(); + const [tasks, setTasks] = useState([]); + const [isPC] = useMediaQuery('(min-width: 768px)', { + ssr: true, + fallback: false // return false on the server, and re-evaluate on the client side + }); + const conf = useConfigStore().commonConfig; + const { taskComponentState, setTaskComponentState } = useDesktopConfigStore(); + const { canShowGuide } = useDesktopConfigStore(); + + useEffect(() => { + const fetchUserTasks = async () => { + await checkUserTask(); + const data = await getUserTasks(); + setTasks(data.data); + }; + fetchUserTasks(); + }, [taskComponentState]); + + useEffect(() => { + const handleUserGuide = async () => { + const data = await getUserTasks(); + setTasks(data.data); + const desktopTask = data.data.find((task) => task.taskType === 'DESKTOP'); + const allTasksCompleted = data.data.every((task) => task.isCompleted); + + if (!desktopTask?.isCompleted && desktopTask?.id) { + setTaskComponentState('none'); + setDesktopGuide(true); + } else if (allTasksCompleted) { + setTaskComponentState('none'); + } else { + setTaskComponentState(taskComponentState !== 'none' ? taskComponentState : 'button'); + } + }; + + if (isPC && conf?.guideEnabled && canShowGuide) { + handleUserGuide(); + } else { + setDesktopGuide(false); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [conf?.guideEnabled, isPC, canShowGuide]); + + const completeGuide = async () => { + try { + if (!tasks.length) return; + setDesktopGuide(false); + const desktopTask = tasks.find((task) => task.taskType === 'DESKTOP'); + if (desktopTask) { + await updateTask(desktopTask.id); + setTaskComponentState('modal'); + } + } catch (error) {} + }; + + const handleCloseTaskModal = () => { + setTaskComponentState('button'); + }; + + const checkAllTasksCompleted = () => { + const allCompleted = tasks.every((task) => task.isCompleted); + if (allCompleted) { + setTaskComponentState('none'); + } + return allCompleted; + }; + + const PopoverBodyInfo = (props: FlexProps) => ( + + + {t('common:click_on_any_shadow_to_skip')} + + + + + + ); + + const driverObj = driver({ + showProgress: false, + allowClose: false, + allowClickMaskNextStep: true, + // allowPreviousStep: true, + isShowButtons: false, + allowKeyboardControl: false, + disableActiveInteraction: true, + steps: [ + { + element: '.apps-container', + popover: { + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', + PopoverBody: ( + + + + + {t('common:application_desktop')} + + + {t('common:application_desktop_tips')} + + + + + ) + } + }, + { + element: '.system-applaunchpad', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_applaunchpad')} + + + + ) + } + }, + { + element: '.system-dbprovider', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_dbprovider')} + + + + ) + } + }, + { + element: '.system-objectstorage', + popover: { + side: 'bottom', + align: 'start', + borderRadius: '0px 12px 12px 12px', + PopoverBody: ( + + + + {t('common:guide_objectstorage')} + + + + ) + } + }, + { + element: '.system-template', + popover: { + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', + PopoverBody: ( + + + + {t('common:launch_various_third-party_applications_with_one_click')} + + + + ) + } + } + ], + onDestroyed: () => { + completeGuide(); + } + }); + + const startGuide = () => { + setDesktopGuide(false); + driverObj.drive(); + }; + + const boxStyles: FlexProps = { + border: '1px solid #69AEFF', + borderRadius: '8px', + padding: '24px', + backgroundColor: '#FFFFFF', + boxShadow: '0px 4px 40px 0px rgba(19, 51, 107, 0.10), 0px 0px 1px 0px rgba(19, 51, 107, 0.10)', + flexDirection: 'column', + maxW: '188px' + }; + + const UserGuide = () => ( + + driver + + + + {t('common:hello_welcome')} + + {layoutConfig?.meta.title} + + 👏 + + + {t('common:you_can_complete_the_following_operations')} + + + + + + + {t('common:usertask.task_launchpad_title')} + + + + {t('common:usertask.task_launchpad_desc')} + + + + + + + {t('common:usertask.task_database_title')} + + + + {t('common:usertask.task_database_desc')} + + + + + + + {t('common:usertask.task_appstore_title')} + + + + {t('common:usertask.task_appstore_desc')} + + + + + + + + + ); + + return { + UserGuide, + desktopGuide, + tasks, + handleCloseTaskModal, + checkAllTasksCompleted, + setTaskComponentState + }; +} diff --git a/frontend/desktop/src/components/team/DissolveTeam.tsx b/frontend/desktop/src/components/team/DissolveTeam.tsx index 0843eab2a18..af3d225da07 100644 --- a/frontend/desktop/src/components/team/DissolveTeam.tsx +++ b/frontend/desktop/src/components/team/DissolveTeam.tsx @@ -1,26 +1,25 @@ +import { deleteTeamRequest } from '@/api/namespace'; +import { useCustomToast } from '@/hooks/useCustomToast'; +import useSessionStore from '@/stores/session'; +import { ApiResp } from '@/types'; import { Button, - Image, + ButtonProps, Modal, ModalBody, ModalCloseButton, ModalContent, ModalHeader, ModalOverlay, - useDisclosure, - Text, Spinner, - ButtonProps + Text, + useDisclosure } from '@chakra-ui/react'; -import CustomInput from './Input'; -import { useState } from 'react'; -import { useCustomToast } from '@/hooks/useCustomToast'; +import { DeleteIcon } from '@sealos/ui'; import { useMutation, useQueryClient } from '@tanstack/react-query'; -import { deleteTeamRequest } from '@/api/namespace'; -import useSessionStore from '@/stores/session'; -import { ApiResp } from '@/types'; import { useTranslation } from 'next-i18next'; -import { DeleteIcon } from '@sealos/ui'; +import { useState } from 'react'; +import CustomInput from './Input'; export default function DissolveTeam({ nsid, ns_uid, @@ -95,7 +94,7 @@ export default function DissolveTeam({ > - Warning + {t('common:warning')} {mutation.isLoading ? ( @@ -108,7 +107,7 @@ export default function DissolveTeam({ e.preventDefault(); setTeamName(e.target.value); }} - placeholder={t('common:name_of_team') || ''} + placeholder={t('common:team') || '' + ' ID'} value={teamName} /> - - -
- ) - } - } - ], - onDestroyed: () => { - console.log('onDestroyed'); - handleSkipGuide(); - } - }); - - const startGuide = () => { - setShowGuide(false); - driverObj.drive(); - }; - - const UserGuide = () => ( - - driver - - - {t('common:hello_welcome')} - - Sealos - - 👏 - - - - - ); - - return { UserGuide, showGuide, startGuide }; -} diff --git a/frontend/desktop/src/pages/WorkspaceInvite.tsx b/frontend/desktop/src/pages/WorkspaceInvite.tsx index 602265d8010..6179d0985a2 100644 --- a/frontend/desktop/src/pages/WorkspaceInvite.tsx +++ b/frontend/desktop/src/pages/WorkspaceInvite.tsx @@ -1,22 +1,17 @@ -import type { NextPage } from 'next'; -import { useRouter } from 'next/router'; -import useSessionStore from '@/stores/session'; -import { Button, Flex, HStack, Image, Spinner, Text, VStack } from '@chakra-ui/react'; -import { isString } from 'lodash'; -import { dehydrate, QueryClient, useMutation, useQuery } from '@tanstack/react-query'; -import { - getInviteCodeInfoRequest, - nsListRequest, - reciveAction, - verifyInviteCodeRequest -} from '@/api/namespace'; +import { getInviteCodeInfoRequest, reciveAction, verifyInviteCodeRequest } from '@/api/namespace'; import useCallbackStore from '@/stores/callback'; -import { useEffect } from 'react'; -import { useTranslation } from 'next-i18next'; +import { useConfigStore } from '@/stores/config'; +import useSessionStore from '@/stores/session'; import { ROLE_LIST } from '@/types/team'; import { compareFirstLanguages } from '@/utils/tools'; +import { Button, Flex, Image, Text, VStack } from '@chakra-ui/react'; +import { dehydrate, QueryClient, useMutation, useQuery } from '@tanstack/react-query'; +import { isString } from 'lodash'; +import type { NextPage } from 'next'; +import { useTranslation } from 'next-i18next'; import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; -import { useConfigStore } from '@/stores/config'; +import { useRouter } from 'next/router'; +import { useEffect } from 'react'; const Callback: NextPage = () => { const router = useRouter(); @@ -70,17 +65,16 @@ const Callback: NextPage = () => { return () => clearTimeout(tag); } }, [statusCode, isValid, router]); - const acceptHandle = async () => { + const acceptHandle = async () => verifyMutation .mutateAsync({ code: inviteCode as string, action: reciveAction.Accepte }) - .then(() => { + .finally(() => { reset(); return; }); - }; const cancelHandle = () => { if (!isValid) return; reset(); @@ -113,7 +107,6 @@ const Callback: NextPage = () => { color={'white'} gap={'8px'} > - {' '} logo Sealos diff --git a/frontend/desktop/src/pages/api/account/checkTask.ts b/frontend/desktop/src/pages/api/account/checkTask.ts new file mode 100644 index 00000000000..fc854ce5026 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/checkTask.ts @@ -0,0 +1,103 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; +import { K8sApi } from '@/services/backend/kubernetes/user'; +import { jsonRes } from '@/services/backend/response'; +import { switchKubeconfigNamespace } from '@/utils/switchKubeconfigNamespace'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { TaskStatus, TaskType } from 'prisma/global/generated/client'; +import * as k8s from '@kubernetes/client-node'; +import { templateDeployKey } from '@/constants/account'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Failed to get info' }); + const namespace = payload.workspaceId; + const _kc = await getUserKubeconfigNotPatch(payload.userCrName); + if (!_kc) return jsonRes(res, { code: 404, message: 'User is not found' }); + const realKc = switchKubeconfigNamespace(_kc, namespace); + const kc = K8sApi(realKc); + if (!kc) return jsonRes(res, { code: 404, message: 'The kubeconfig is not found' }); + + const k8sApp = kc.makeApiClient(k8s.AppsV1Api); + const k8sCustomObjects = kc.makeApiClient(k8s.CustomObjectsApi); + + const userTasks = await globalPrisma.userTask.findMany({ + where: { + userUid: payload.userUid, + status: { not: TaskStatus.COMPLETED } + }, + include: { task: true } + }); + + const [deployments, statefulsets, instances, clusters] = await Promise.all([ + k8sApp.listNamespacedDeployment( + namespace, + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ), + k8sApp.listNamespacedStatefulSet( + namespace, + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ), + k8sCustomObjects.listNamespacedCustomObject( + 'app.sealos.io', + 'v1', + namespace, + 'instances' + ) as any, + k8sCustomObjects.listNamespacedCustomObject( + 'apps.kubeblocks.io', + 'v1alpha1', + namespace, + 'clusters', + undefined, + undefined, + undefined, + undefined, + `!${templateDeployKey}` + ) as any + ]); + + const tasksToUpdate = userTasks.filter((userTask) => { + switch (userTask.task.taskType) { + case TaskType.LAUNCHPAD: + return deployments.body.items.length > 0 || statefulsets.body.items.length > 0; + case TaskType.APPSTORE: + return instances.body.items.length > 0; + case TaskType.DATABASE: + return clusters.body.items.length > 0; + default: + return false; + } + }); + + if (tasksToUpdate.length > 0) { + await globalPrisma.userTask.updateMany({ + where: { + OR: tasksToUpdate.map((task) => ({ + userUid: task.userUid, + taskId: task.taskId + })) + }, + data: { + status: TaskStatus.COMPLETED, + completedAt: new Date() + } + }); + } + + jsonRes(res, { code: 200, data: `${tasksToUpdate.length} tasks updated successfully` }); + } catch (error) { + console.error('Error processing request:', error); + jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts b/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts new file mode 100644 index 00000000000..dcbb683ad37 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/enterpriseRealNameAuth.ts @@ -0,0 +1,245 @@ +import { jsonRes } from '@/services/backend/response'; +import { enableEnterpriseRealNameAuth } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { RealNameOSSConfigType } from '@/types'; +import * as Minio from 'minio'; +import formidable, { Fields, Files, File, Part } from 'formidable'; +import path from 'path'; +import Formidable from 'formidable/Formidable'; +import fs from 'fs/promises'; + +export const config = { + api: { + bodyParser: false + } +}; + +const realNameOSS: RealNameOSSConfigType = global.AppConfig.realNameOSS; + +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB +const ALLOWED_FILE_TYPES = ['image/jpeg', 'image/png', 'application/pdf']; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableEnterpriseRealNameAuth) { + console.error('enterpriseRealNameAuth: enterprise real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Enterprise real name authentication not enabled' }); + } + + if (req.method !== 'POST') { + console.error('enterpriseRealNameAuth: Method not allowed'); + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + + if (!realNameOSS) { + return jsonRes(res, { + code: 500, + message: 'Real name authentication oss configuration not found' + }); + } + + try { + const userUid = payload.userUid; + + const form = formidable({ + multiples: false, + keepExtensions: true, + maxFileSize: MAX_FILE_SIZE, + filter: function (part: Part): boolean { + return part.mimetype !== null && ALLOWED_FILE_TYPES.includes(part.mimetype); + }, + filename: function (name: string, ext: string, part: Part, form: Formidable): string { + const sanitizedName = sanitizeFilename(part.originalFilename || 'unnamed'); + return sanitizedName; + } + }); + + const formData = await parseFormData(req, form); + const { fields, files } = formData; + + const enterpriseName = fields.enterpriseName?.[0]; + + if ((enterpriseName && enterpriseName.length < 1) || enterpriseName.length > 20) { + return jsonRes(res, { + code: 400, + message: 'Enterprise name must be between 1 and 20 characters' + }); + } + + if (!files.enterpriseQualification?.[0] || !files.supportingMaterials?.[0]) { + return jsonRes(res, { + code: 400, + message: 'Enterprise qualification and supporting materials are required' + }); + } + + if ( + files && + files.enterpriseQualification?.[0] && + files.enterpriseQualification?.[0].size > MAX_FILE_SIZE + ) { + return jsonRes(res, { + code: 400, + message: 'Enterprise qualification file size exceeds the maximum limit' + }); + } + + if ( + files && + files.supportingMaterials?.[0] && + files.supportingMaterials?.[0].size > MAX_FILE_SIZE + ) { + return jsonRes(res, { + code: 400, + message: 'Supporting materials file size exceeds the maximum limit' + }); + } + + // Check if EnterpriseRealNameInfo exists + const existingInfo = await globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { userUid } + }); + + if (!existingInfo) { + // Create new EnterpriseRealNameInfo + const ossPaths = await uploadFiles(userUid, files); + await createEnterpriseRealNameInfo(userUid, enterpriseName, ossPaths); + return jsonRes(res, { + code: 200, + message: 'Enterprise real name authentication submitted successfully', + data: { status: 'Pending' } + }); + } + + // Handle existing EnterpriseRealNameInfo cases + switch (existingInfo.verificationStatus) { + case 'Pending': + return jsonRes(res, { code: 400, message: 'Authentication is under review' }); + case 'Success': + return jsonRes(res, { code: 400, message: 'Cannot authenticate multiple times' }); + case 'Failed': + // Re-upload files and update EnterpriseRealNameInfo + const newOssPaths = await uploadFiles(userUid, files); + await updateEnterpriseRealNameInfo(existingInfo.id, enterpriseName, newOssPaths); + return jsonRes(res, { + code: 200, + data: { status: 'Pending' }, + message: 'Enterprise real name authentication resubmitted successfully' + }); + default: + return jsonRes(res, { code: 500, message: 'Invalid verification status' }); + } + } catch (error) { + console.error('enterpriseRealNameAuth: Internal error', error); + return jsonRes(res, { code: 500, message: 'The server has encountered an error' }); + } +} + +// Helper functions +async function parseFormData(req: NextApiRequest, form: Formidable): Promise { + return new Promise((resolve, reject) => { + form.parse(req, (err: Error, fields: Fields, files: Files) => { + if (err) { + reject(err); + } else { + resolve({ fields, files }); + } + }); + }); +} + +function sanitizeFilename(filename: string): string { + // Remove any path components + const basename = path.basename(filename); + // Define a blacklist of malicious characters + const blacklist = /[<>:"/\\|?*\x00-\x1F]/g; + // Replace blacklisted characters with underscores + return basename.replace(blacklist, '_'); +} + +async function uploadFiles(userUid: string, files: Files): Promise { + const minioConfig: Minio.ClientOptions = { + endPoint: realNameOSS.endpoint, + accessKey: realNameOSS.accessKey, + secretKey: realNameOSS.accessKeySecret, + useSSL: realNameOSS.ssl + }; + const minioClient = new Minio.Client(minioConfig); + + const filesToUpload = [ + { file: files.enterpriseQualification?.[0], name: 'enterpriseQualification' }, + { file: files.supportingMaterials?.[0], name: 'supportingMaterials' } + ].filter((item) => item.file !== null); + + const uploadPromises = filesToUpload.map((item) => + uploadFile(minioClient, userUid, item.file!, item.name) + ); + + return await Promise.all(uploadPromises); +} + +async function uploadFile( + minioClient: Minio.Client, + userUid: string, + file: File, + fileType: string +): Promise { + const timestamp = Date.now(); + const fileName = `${timestamp}_${fileType}_${file.newFilename}`; + const filePath = `/${userUid}/${fileName}`; + try { + await minioClient.fPutObject(realNameOSS.enterpriseRealNameBucket, filePath, file.filepath, { + 'Content-Type': file.mimetype || 'application/octet-stream' + }); + + // Check if the file exists before attempting to delete it + try { + await fs.access(file.filepath); + // If no error is thrown, the file exists, so we can delete it + await fs.unlink(file.filepath); + console.debug(`File ${file.filepath} has been deleted.`); + } catch (accessError) { + // If an error is thrown, the file doesn't exist + console.debug(`File ${file.filepath} does not exist or is not accessible.`); + } + } catch (error) { + console.error('EnterpriseRealNameAuth uploadFile: Error uploading file', error); + throw error; + } + + return filePath; +} + +async function createEnterpriseRealNameInfo( + userUid: string, + enterpriseName: string, + ossPaths: string[] +) { + await globalPrisma.enterpriseRealNameInfo.create({ + data: { + userUid, + enterpriseName: enterpriseName, + supportingMaterials: { ossPaths: ossPaths }, // Remaining paths for supportingMaterials + verificationStatus: 'Pending' + } + }); +} + +async function updateEnterpriseRealNameInfo( + id: string, + enterpriseName: string, + ossPaths: string[] +) { + await globalPrisma.enterpriseRealNameInfo.update({ + where: { id }, + data: { + enterpriseName: enterpriseName, + supportingMaterials: { ossPaths: ossPaths }, + verificationStatus: 'Pending' + } + }); +} diff --git a/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts new file mode 100644 index 00000000000..c265dd6394d --- /dev/null +++ b/frontend/desktop/src/pages/api/account/faceIdRealNameAuthCallback.ts @@ -0,0 +1,308 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { enableRealNameAuth } from '@/services/enable'; +import * as tcsdk from 'tencentcloud-sdk-nodejs'; +import { NextApiRequest, NextApiResponse } from 'next'; +import { globalPrisma } from '@/services/backend/db/init'; +import { GetDetectInfoEnhancedResponse } from 'tencentcloud-sdk-nodejs/tencentcloud/services/faceid/v20180301/faceid_models'; +import { RealNameOSSConfigType } from '@/types'; +import { Client, ClientOptions } from 'minio'; + +type TencentCloudFaceAuthConfig = { + secretId: string; + secretKey: string; + ruleId: string; +}; + +type JsonValue = string | number | boolean | object | null; + +type RealNameAuthProvider = { + id: string; + backend: string; + authType: string; + maxFailedTimes: number; + config: JsonValue; + createdAt: Date; + updatedAt: Date; +}; + +type AdditionalInfo = + | { + faceRecognition?: { + callback?: { + bizToken?: string; + url?: string | null; + isUsed?: boolean; + createdAt?: number; + }; + }; + userMaterials?: string[]; + } + | any; + +const realNameOSS: RealNameOSSConfigType = global.AppConfig.realNameOSS; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableRealNameAuth) { + console.error('faceidRealNameAuth: Real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Real name authentication not enabled' }); + } + + const bizToken = req.query?.BizToken as string; + const extraQuery = req.query?.Extra as string; + + const regionToken = extraQuery?.split('regionToken=')[1]; + if (!regionToken) { + return jsonRes(res, { code: 400, message: 'Token is required' }); + } + + req.headers['authorization'] = regionToken; + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); + + if (!realNameOSS) { + return jsonRes(res, { + code: 500, + message: 'Real name authentication oss configuration not found' + }); + } + + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'TENCENTCLOUD', + authType: 'tcloudFaceAuth' + } + }); + + if (!realNameAuthProvider) { + throw new Error('faceidRealNameAuth: Real name authentication provider not found'); + } + + const config: TencentCloudFaceAuthConfig = + realNameAuthProvider.config as TencentCloudFaceAuthConfig; + + if (!config) { + throw new Error('faceidRealNameAuth: Real name authentication configuration not found'); + } + + const userRealNameFaceAuthInfo = await getUserRealNameInfo(bizToken, config); + const isFaceRecognitionSuccess = userRealNameFaceAuthInfo.Text?.ErrCode === 0; + + const userUid = payload.userUid; + const timestamp = Date.now(); + + // Fetch existing user real name info + const userRealNameInfo = await globalPrisma.userRealNameInfo.findUnique({ + where: { userUid } + }); + + if (!userRealNameInfo || !userRealNameInfo.additionalInfo) { + return jsonRes(res, { code: 400, message: 'User real name info not found' }); + } + + let additionalInfo: AdditionalInfo = userRealNameInfo.additionalInfo; + + additionalInfo.faceRecognition.callback.isUsed = true; + + // Initialize or reset userMaterials array + additionalInfo.userMaterials = []; + + const minioConfig: ClientOptions = { + endPoint: realNameOSS.endpoint, + accessKey: realNameOSS.accessKey, + secretKey: realNameOSS.accessKeySecret, + useSSL: realNameOSS.ssl + }; + const minioClient = new Client(minioConfig); + + if (userRealNameFaceAuthInfo.BestFrame?.BestFrame) { + const imageBuffer = Buffer.from(userRealNameFaceAuthInfo.BestFrame.BestFrame, 'base64'); + const imagePath = `${userUid}/${timestamp}_bestframe.jpg`; + await uploadFile( + minioClient, + realNameOSS.realNameBucket, + imagePath, + imageBuffer, + 'image/jpeg' + ); + additionalInfo.userMaterials.push(imagePath); + } + + if (userRealNameFaceAuthInfo.VideoData?.LivenessVideo) { + const videoBuffer = Buffer.from(userRealNameFaceAuthInfo.VideoData.LivenessVideo, 'base64'); + const videoPath = `${userUid}/${timestamp}_video.mp4`; + await uploadFile( + minioClient, + realNameOSS.realNameBucket, + videoPath, + videoBuffer, + 'video/mp4' + ); + additionalInfo.userMaterials.push(videoPath); + } + + if (isFaceRecognitionSuccess) { + await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + realName: userRealNameFaceAuthInfo.Text?.Name, + idCard: userRealNameFaceAuthInfo.Text?.IdCard, + isVerified: true, + additionalInfo + } + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Successful

+ + + `); + } else { + await globalPrisma.userRealNameInfo.update({ + where: { userUid }, + data: { + isVerified: false, + idVerifyFailedTimes: { increment: 1 }, + additionalInfo + } + }); + + res.setHeader('Content-Type', 'text/html'); + return res.send(` + + + + + + Real Name Authentication + + + +

Real Name Authentication Failed

+ + + `); + } + } catch (error) { + console.error('faceidRealNameAuth: Internal error'); + console.error(error); + res.setHeader('Content-Type', 'text/html'); + return res.status(500).send(` + + + + + + Server Error + + + +
+

Server Error

+

The server has encountered an error. Please try again later.

+
+ + + `); + } +} + +async function getUserRealNameInfo( + bizToken: string, + config: TencentCloudFaceAuthConfig +): Promise { + const FaceClient = tcsdk.faceid.v20180301.Client; + const client = new FaceClient({ + credential: { + secretId: config.secretId, + secretKey: config.secretKey + }, + region: '', + profile: { + signMethod: 'HmacSHA256', + httpProfile: { + endpoint: 'faceid.tencentcloudapi.com', + reqMethod: 'POST', + reqTimeout: 30 // Request timeout, default 60s + } + } + }); + + const params = { + BizToken: bizToken, + InfoType: '0', + RuleId: config.ruleId, + BestFramesCount: 0 + }; + + const data = await client.GetDetectInfoEnhanced(params); + return data; +} + +async function uploadFile( + minioClient: Client, + bucket: string, + path: string, + buffer: Buffer, + contentType: string +): Promise { + await minioClient.putObject(bucket, path, buffer, buffer.length, { 'Content-Type': contentType }); +} diff --git a/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts b/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts new file mode 100644 index 00000000000..3120f8a859a --- /dev/null +++ b/frontend/desktop/src/pages/api/account/generateRealNameQRcodeUri.ts @@ -0,0 +1,210 @@ +import { jsonRes } from '@/services/backend/response'; +import { enableRealNameAuth } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import * as tcsdk from 'tencentcloud-sdk-nodejs'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; + +type TencentCloudFaceAuthConfig = { + secretId: string; + secretKey: string; + ruleId: string; +}; + +type JsonValue = string | number | boolean | object | null; + +type RealNameAuthProvider = { + id: string; + backend: string; + authType: string; + maxFailedTimes: number; + config: JsonValue; + createdAt: Date; + updatedAt: Date; +}; + +type AdditionalInfo = + | { + faceRecognition?: { + callback?: { + bizToken?: string; + url?: string | null; + isUsed?: boolean; + createdAt?: number; + }; + }; + userMaterials?: string[]; + } + | any; + +type QRCodeUrlResult = { + url: string; + bizToken: string; +}; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableRealNameAuth) { + console.error('faceidRealNameAuth: Real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Real name authentication not enabled' }); + } + + if (req.method !== 'GET') { + console.error('faceidRealNameAuth: Method not allowed'); + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); + + try { + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'TENCENTCLOUD', + authType: 'tcloudFaceAuth' + } + }); + + if (!realNameAuthProvider) { + throw new Error('faceidRealNameAuth: Real name authentication provider not found'); + } + + const config: TencentCloudFaceAuthConfig = + realNameAuthProvider.config as TencentCloudFaceAuthConfig; + + if (!config) { + throw new Error('faceidRealNameAuth: Real name authentication configuration not found'); + } + + const realNameInfo = await globalPrisma.userRealNameInfo.findUnique({ + where: { + userUid: payload.userUid + } + }); + + if (realNameInfo && realNameInfo.isVerified) { + console.info(`faceidRealNameAuth: User ${payload.userUid} has already been verified`); + return jsonRes(res, { + code: 409, + message: 'Identity verification has been completed, cannot be repeated.' + }); + } + + if (realNameInfo && realNameInfo.idVerifyFailedTimes >= realNameAuthProvider.maxFailedTimes) { + console.info( + `faceidRealNameAuth: User ${payload.userUid} has reached the maximum number of failed attempts` + ); + return jsonRes(res, { + code: 429, + message: 'You have exceeded the maximum number of attempts. Please submit a ticket' + }); + } + + let urlResult: QRCodeUrlResult | null = null; + let additionalInfo: AdditionalInfo = realNameInfo?.additionalInfo || {}; + + const currentTime = new Date().getTime(); + const urlCreatedAt = additionalInfo.faceRecognition?.callback?.createdAt || 0; + const urlExpirationTime = 7200 * 1000; + + /* If the user has not been authenticated, or the authentication link has expired, + or the authentication link has already been used, + the authentication link needs to be regenerated. + */ + + const shouldGenerateNewUrl = + !realNameInfo || + !additionalInfo.faceRecognition?.callback?.url || + additionalInfo.faceRecognition?.callback?.isUsed || + currentTime - urlCreatedAt > urlExpirationTime; + + if (shouldGenerateNewUrl) { + const redirectUrl = `https://${global.AppConfig?.cloud.domain}/api/account/faceIdRealNameAuthCallback`; + const regionToken = req.headers['authorization'] as string; + urlResult = await generateRealNameQRcodeUri( + redirectUrl, + regionToken, + config as TencentCloudFaceAuthConfig + ); + + additionalInfo = { + ...additionalInfo, + faceRecognition: { + ...additionalInfo.faceRecognition, + callback: { + bizToken: urlResult.bizToken, + url: urlResult.url, + isUsed: false, + createdAt: currentTime + } + } + }; + + await globalPrisma.userRealNameInfo.upsert({ + where: { userUid: payload.userUid }, + update: { additionalInfo }, + create: { + userUid: payload.userUid, + isVerified: false, + idVerifyFailedTimes: 0, + additionalInfo + } + }); + } else { + urlResult = { + url: additionalInfo.faceRecognition?.callback?.url || null, + bizToken: additionalInfo.faceRecognition?.callback?.bizToken || null + }; + } + + return jsonRes(res, { + code: 200, + message: 'success generate real name auth url', + data: { url: urlResult.url, bizToken: urlResult.bizToken } + }); + } catch (error) { + console.error('faceidRealNameAuth: Internal error'); + console.error(error); + return jsonRes(res, { code: 500, data: 'The server has encountered an error' }); + } +} + +async function generateRealNameQRcodeUri( + redirectUrl: string, + regionToken: string, + config: TencentCloudFaceAuthConfig +): Promise { + const FaceClient = tcsdk.faceid.v20180301.Client; + const client = new FaceClient({ + credential: { + secretId: config.secretId, + secretKey: config.secretKey + }, + region: '', + profile: { + signMethod: 'HmacSHA256', + httpProfile: { + endpoint: 'faceid.tencentcloudapi.com', + reqMethod: 'POST', + reqTimeout: 30 // Request timeout, default 60s + } + } + }); + + const params = { + RuleId: config.ruleId, + RedirectUrl: redirectUrl, + Extra: `regionToken=${regionToken}` + }; + + const data = await client.DetectAuth(params); + + if (!data.Url || !data.BizToken) { + throw new Error('Failed to generate QR code URL: Missing Url or BizToken'); + } + + return { + url: data.Url, + bizToken: data.BizToken + }; +} diff --git a/frontend/desktop/src/pages/api/account/getAccount.ts b/frontend/desktop/src/pages/api/account/getAccount.ts deleted file mode 100644 index 7db135f8258..00000000000 --- a/frontend/desktop/src/pages/api/account/getAccount.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { GetCRD, K8sApi } from '@/services/backend/kubernetes/user'; -import { jsonRes } from '@/services/backend/response'; -import { CRDMeta } from '@/types'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { getUserKubeconfigNotPatch, K8sApiDefault } from '@/services/backend/kubernetes/admin'; -import { verifyAccessToken } from '@/services/backend/auth'; -export const AccountMeta: CRDMeta = { - group: 'account.sealos.io', - version: 'v1', - namespace: 'sealos-system', - plural: 'accounts' -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'token is invaild' }); - const kc = await getUserKubeconfigNotPatch(payload.userCrName); - if (!kc) return jsonRes(res, { code: 404, message: ' kubeconfig is not found' }); - const result = await GetCRD(K8sApiDefault(), AccountMeta, payload.userCrName); - jsonRes(res, { data: result?.body }); - } catch (error) { - console.log(error); - jsonRes(res, { code: 500, data: error }); - } -} diff --git a/frontend/desktop/src/pages/api/account/getFaceAuthStatus.ts b/frontend/desktop/src/pages/api/account/getFaceAuthStatus.ts new file mode 100644 index 00000000000..2382381e913 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/getFaceAuthStatus.ts @@ -0,0 +1,144 @@ +import { jsonRes } from '@/services/backend/response'; +import { enableRealNameAuth } from '@/services/enable'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import * as tcsdk from 'tencentcloud-sdk-nodejs'; +import { verifyAccessToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { z } from 'zod'; +import { GetDetectInfoEnhancedResponse } from 'tencentcloud-sdk-nodejs/tencentcloud/services/faceid/v20180301/faceid_models'; + +type TencentCloudFaceAuthConfig = { + secretId: string; + secretKey: string; + ruleId: string; +}; + +type JsonValue = string | number | boolean | object | null; + +type RealNameAuthProvider = { + id: string; + backend: string; + authType: string; + maxFailedTimes: number; + config: JsonValue; + createdAt: Date; + updatedAt: Date; +}; + +enum FaceAuthStatus { + SUCCESS = 'Success', + FAIL = 'Failed', + PENDING = 'Pending' +} + +type FaceAuthResult = { + status: FaceAuthStatus; + realName?: string; +}; + +const bodySchema = z.object({ + bizToken: z.string().length(36, { message: 'bizToken must be exactly 36 characters long' }) +}); + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!enableRealNameAuth) { + console.error('faceidRealNameAuth: Real name authentication not enabled'); + return jsonRes(res, { code: 503, message: 'Real name authentication not enabled' }); + } + + if (req.method !== 'POST') { + console.error('realNameAuth: Method not allowed'); + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const payload = await verifyAccessToken(req.headers); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); + + const faceAuthResult: FaceAuthResult = { + status: FaceAuthStatus.PENDING + }; + + try { + const { bizToken } = bodySchema.parse(req.body); + + const realNameAuthProvider: RealNameAuthProvider | null = + await globalPrisma.realNameAuthProvider.findFirst({ + where: { + backend: 'TENCENTCLOUD', + authType: 'tcloudFaceAuth' + } + }); + + if (!realNameAuthProvider) { + throw new Error('faceidRealNameAuth: Real name authentication provider not found'); + } + + const config: TencentCloudFaceAuthConfig = + realNameAuthProvider.config as TencentCloudFaceAuthConfig; + + if (!config) { + throw new Error('faceidRealNameAuth: Real name authentication configuration not found'); + } + + const faceAuthInfo = await getUserRealNameInfo(bizToken, config); + + const realNameInfo = await globalPrisma.userRealNameInfo.findUnique({ + where: { + userUid: payload.userUid + } + }); + + if (faceAuthInfo.Text?.ErrCode !== null && faceAuthInfo.Text?.ErrCode === 0) { + if (realNameInfo && realNameInfo.realName && realNameInfo.isVerified) { + faceAuthResult.status = FaceAuthStatus.SUCCESS; + faceAuthResult.realName = realNameInfo.realName; + } + } + + if (faceAuthInfo.Text?.ErrCode !== null && faceAuthInfo.Text?.ErrCode !== 0) { + faceAuthResult.status = FaceAuthStatus.FAIL; + } + + return jsonRes(res, { + code: 200, + message: 'success get face auth result', + data: { status: faceAuthResult.status, realName: faceAuthResult.realName } + }); + } catch (error) { + console.error('faceidRealNameAuth: Internal error'); + console.error(error); + return jsonRes(res, { code: 500, data: 'The server has encountered an error' }); + } +} + +async function getUserRealNameInfo( + bizToken: string, + config: TencentCloudFaceAuthConfig +): Promise { + const FaceClient = tcsdk.faceid.v20180301.Client; + const client = new FaceClient({ + credential: { + secretId: config.secretId, + secretKey: config.secretKey + }, + region: '', + profile: { + signMethod: 'HmacSHA256', + httpProfile: { + endpoint: 'faceid.tencentcloudapi.com', + reqMethod: 'POST', + reqTimeout: 30 // Request timeout, default 60s + } + } + }); + + const params = { + BizToken: bizToken, + InfoType: '0', + RuleId: config.ruleId, + BestFramesCount: 0 + }; + + const data = await client.GetDetectInfoEnhanced(params); + return data; +} diff --git a/frontend/desktop/src/pages/api/account/getTasks.ts b/frontend/desktop/src/pages/api/account/getTasks.ts new file mode 100644 index 00000000000..0dab32e57cb --- /dev/null +++ b/frontend/desktop/src/pages/api/account/getTasks.ts @@ -0,0 +1,49 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); + + const userTasks = await globalPrisma.userTask.findMany({ + where: { + userUid: payload.userUid, + task: { + isActive: true + } + }, + include: { + task: true + }, + orderBy: { + task: { + order: 'asc' + } + } + }); + + const tasks = userTasks.map((ut) => ({ + id: ut.task.id, + title: JSON.parse(ut.task.title), + description: ut.task.description, + reward: ut.task.reward.toString(), + order: ut.task.order, + taskType: ut.task.taskType, + isCompleted: ut.status === 'COMPLETED', + completedAt: ut.completedAt + })); + + const allTasksCompleted = tasks.every((task) => task.isCompleted); + + jsonRes(res, { + code: 200, + data: allTasksCompleted ? [] : tasks, + message: allTasksCompleted ? 'All tasks completed' : 'Tasks fetched' + }); + } catch (error) { + return jsonRes(res, { code: 500, message: 'error' }); + } +} diff --git a/frontend/desktop/src/pages/api/account/realNameAuth.ts b/frontend/desktop/src/pages/api/account/realNameAuth.ts deleted file mode 100644 index 100e718bf47..00000000000 --- a/frontend/desktop/src/pages/api/account/realNameAuth.ts +++ /dev/null @@ -1,219 +0,0 @@ -import { jsonRes } from '@/services/backend/response'; -import { enableRealNameAuth } from '@/services/enable'; -import { z } from 'zod'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import * as tcsdk from 'tencentcloud-sdk-nodejs'; -import { verifyAccessToken } from '@/services/backend/auth'; -import { identityCodeValid } from '@/utils/tools'; -import { globalPrisma } from '@/services/backend/db/init'; - -type TencentCloudPhone3efConfig = { - secretId: string; - secretKey: string; -}; - -// type OtherBackendConfig = { ... }; - -// backend_authType -type ConfigMap = { - TENCENTCLOUD_tcloudphone3ef: TencentCloudPhone3efConfig; - // 'OTHER_BACKEND_authType': OtherBackendConfig; -}; - -type RealNameAuthProvider = { - id: string; - backend: string; - authType: string; - maxFailedTimes: number; - config: ConfigType; - createdAt: Date; - updatedAt: Date; -}; - -type ConfigType = { - [K in keyof ConfigMap]: RealNameAuthProvider extends { backend: infer B; authType: infer A } - ? `${B extends string ? B : never}_${A extends string ? A : never}` extends K - ? ConfigMap[K] - : never - : never; -}[keyof ConfigMap]; - -const bodySchema = z.object({ - name: z - .string() - .min(1, { message: 'Name must not be empty' }) - .max(20, { message: 'Name must not exceed 20 characters' }), - idCard: z.string().refine(identityCodeValid, { message: 'Invalid ID card number' }) -}); - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableRealNameAuth) { - console.error('realNameAuth: Real name authentication not enabled'); - return jsonRes(res, { code: 503, message: 'Real name authentication not enabled' }); - } - - if (req.method !== 'POST') { - console.error('realNameAuth: Method not allowed'); - return jsonRes(res, { code: 405, message: 'Method not allowed' }); - } - - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'Token is invaild' }); - - try { - const { name, idCard } = bodySchema.parse(req.body); - - const oauthProvider = await globalPrisma.oauthProvider.findFirst({ - where: { - userUid: payload.userUid, - providerType: 'PHONE' - } - }); - - if (!oauthProvider) { - console.error('realNameAuth: User has not bound phone number'); - return jsonRes(res, { code: 400, message: 'Mobile number not bound' }); - } - - const phone = oauthProvider.providerId; - - const realNameAuthProvider = (await globalPrisma.realNameAuthProvider.findFirst({ - where: { - backend: 'TENCENTCLOUD', - authType: 'tcloudphone3ef' - } - })) as RealNameAuthProvider | null; - - const config = realNameAuthProvider?.config; - - if (!config) { - throw new Error('realNameAuth: Real name authentication configuration not found'); - } - - const realNameInfo = await globalPrisma.userRealNameInfo.findUnique({ - where: { - userUid: payload.userUid - } - }); - - if (realNameInfo && realNameInfo.isVerified) { - console.info(`realNameAuth: User ${payload.userUid} has already been verified`); - return jsonRes(res, { - code: 409, - message: 'Identity verification has been completed, cannot be repeated.' - }); - } - - if (realNameInfo && realNameInfo.idVerifyFailedTimes >= realNameAuthProvider.maxFailedTimes) { - console.info( - `realNameAuth: User ${payload.userUid} has reached the maximum number of failed attempts` - ); - return jsonRes(res, { - code: 429, - message: 'You have exceeded the maximum number of attempts. Please submit a ticket' - }); - } - - const { code, data } = await tcloudphone3efVerifyService(phone, name, idCard, config); - - /* '-4' and '-5' are the results of chargeable interfaces, - and the number of failures is recorded for subsequent limitation. - */ - if (code === '-4' || code === '-5') { - await globalPrisma.userRealNameInfo.upsert({ - where: { userUid: payload.userUid }, - update: { - realName: name, - idCard: idCard, - phone: phone, - idVerifyFailedTimes: { - increment: 1 - }, - updatedAt: new Date() - }, - create: { - userUid: payload.userUid, - realName: name, - idCard: idCard, - phone: phone, - idVerifyFailedTimes: 1, - isVerified: false, - createdAt: new Date(), - updatedAt: new Date() - } - }); - } - - if (code !== 0) { - console.info( - `realNameAuth: Real name authentication failed,useruid ${payload.userUid} code:${code} data:${data}` - ); - return jsonRes(res, { - code: 400, - message: - 'Identity verification failed. Please ensure that the name, ID number, and mobile number are consistent' - }); - } - - await globalPrisma.userRealNameInfo.upsert({ - where: { userUid: payload.userUid }, - update: { - realName: name, - idCard: idCard, - phone: phone, - isVerified: true, - updatedAt: new Date() - }, - create: { - userUid: payload.userUid, - realName: name, - idCard: idCard, - phone: phone, - idVerifyFailedTimes: 0, - isVerified: true, - createdAt: new Date(), - updatedAt: new Date() - } - }); - - return jsonRes(res, { code: 200, message: 'Identity verification success', data: { name } }); - } catch (error) { - console.error('realNameAuth: Internal error'); - console.error(error); - return jsonRes(res, { code: 500, data: 'The server has encountered an error' }); - } -} - -async function tcloudphone3efVerifyService( - phone: string, - name: string, - idCard: string, - config: TencentCloudPhone3efConfig -) { - const FaceClient = tcsdk.faceid.v20180301.Client; - const client = new FaceClient({ - credential: { - secretId: config.secretId, - secretKey: config.secretKey - }, - profile: { - signMethod: 'HmacSHA256', - httpProfile: { - reqMethod: 'POST', - reqTimeout: 30 // Request timeout, default 60s - } - } - }); - - const res = await client.PhoneVerification({ - Phone: phone, - IdCard: idCard, - Name: name - }); - - if (res?.Result !== '0') { - return { code: res?.Result, data: res?.Description }; - } - - return { code: 0, data: res?.Description }; -} diff --git a/frontend/desktop/src/pages/api/account/updateGuide.ts b/frontend/desktop/src/pages/api/account/updateGuide.ts deleted file mode 100644 index 8716e1afd02..00000000000 --- a/frontend/desktop/src/pages/api/account/updateGuide.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { K8sApiDefault } from '@/services/backend/kubernetes/admin'; -import { UpdateCRD } from '@/services/backend/kubernetes/user'; -import { jsonRes } from '@/services/backend/response'; -import type { NextApiRequest, NextApiResponse } from 'next'; -import { AccountMeta } from './getAccount'; -import { GUIDE_DESKTOP_INDEX_KEY } from '@/constants/account'; -import { verifyAccessToken } from '@/services/backend/auth'; - -// req header is kubeconfig -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - const payload = await verifyAccessToken(req.headers); - if (!payload) return jsonRes(res, { code: 401, message: 'token is vaild' }); - - const defaultKc = K8sApiDefault(); - - if (!defaultKc) return jsonRes(res, { code: 401, message: 'No cluster permissions' }); - - const endTime = new Date().toISOString(); - - const jsonPatch = [ - { - op: 'add', - path: `/metadata/annotations/${GUIDE_DESKTOP_INDEX_KEY}`, - value: endTime - } - ]; - - const reuslt = await UpdateCRD(defaultKc, AccountMeta, payload.userCrName, jsonPatch); - - jsonRes(res, { data: reuslt?.body }); - } catch (error) { - jsonRes(res, { code: 500, data: error }); - } -} diff --git a/frontend/desktop/src/pages/api/account/updateTask.ts b/frontend/desktop/src/pages/api/account/updateTask.ts new file mode 100644 index 00000000000..e010aad8e50 --- /dev/null +++ b/frontend/desktop/src/pages/api/account/updateTask.ts @@ -0,0 +1,52 @@ +import { verifyAccessToken, verifyAppToken } from '@/services/backend/auth'; +import { globalPrisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import type { NextApiRequest, NextApiResponse } from 'next'; +import { TaskStatus } from 'prisma/global/generated/client'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + const payload = (await verifyAccessToken(req.headers)) || (await verifyAppToken(req.headers)); + if (!payload) return jsonRes(res, { code: 401, message: 'Token is invalid' }); + + if (req.method !== 'POST') { + return jsonRes(res, { code: 405, message: 'Method not allowed' }); + } + + const { taskId } = req.body as { taskId: string }; + if (!taskId) { + return jsonRes(res, { code: 400, message: 'Task ID is required' }); + } + + const task = await globalPrisma.userTask.findUnique({ + where: { + userUid_taskId: { + userUid: payload.userUid, + taskId: taskId + } + } + }); + + if (!task) { + return jsonRes(res, { code: 404, message: 'Task not found' }); + } + + if (task.status === TaskStatus.COMPLETED) { + return jsonRes(res, { code: 200, message: 'Task is already completed' }); + } + + const updatedTask = await globalPrisma.userTask.update({ + where: { + userUid_taskId: { + userUid: payload.userUid, + taskId: taskId + } + }, + data: { status: TaskStatus.COMPLETED, completedAt: new Date() } + }); + + jsonRes(res, { code: 200, data: 'success' }); + } catch (error) { + jsonRes(res, { code: 500, message: 'Internal server error' }); + } +} diff --git a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts index 9c1f568d09c..5c10e97502f 100644 --- a/frontend/desktop/src/pages/api/auth/email/bind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/bind/verify.ts @@ -1,13 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; -import { verifyEmailCodeGuard, filterEmailVerifyParams } from '@/services/backend/middleware/sms'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { bindEmailSvc, bindPhoneSvc } from '@/services/backend/svc/bindProvider'; import { ErrorHandler } from '@/services/backend/middleware/error'; -import { bindEmailGuard, bindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { bindEmailGuard } from '@/services/backend/middleware/oauth'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { bindEmailSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts index faf1647b9e6..0413e4c4e73 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/newSms.ts @@ -1,17 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { - verifyCodeUidGuard, - filterEmailParams, - sendEmailCodeGuard, filterCodeUid, + filterEmailParams, sendNewEmailCodeGuard } from '@/services/backend/middleware/sms'; import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts index 08958e261ee..d9c3dc24ddf 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/oldSms.ts @@ -1,13 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { sendEmailCodeGuard, filterEmailParams } from '@/services/backend/middleware/sms'; -import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; +import { filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts index b9e6c7d7356..b39eeba9cc5 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyNew.ts @@ -1,18 +1,18 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindEmailGuard, unbindEmailGuard } from '@/services/backend/middleware/oauth'; import { - verifyCodeUidGuard, - verifyEmailCodeGuard, + filterCodeUid, filterEmailVerifyParams, - filterCodeUid + verifyCodeUidGuard, + verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { changeEmailBindingSvc } from '@/services/backend/svc/bindProvider'; -import { ErrorHandler } from '@/services/backend/middleware/error'; -import { bindEmailGuard, unbindEmailGuard } from '@/services/backend/middleware/oauth'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts index cb3adc68db8..0a27b76d207 100644 --- a/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts +++ b/frontend/desktop/src/pages/api/auth/email/changeBinding/verifyOld.ts @@ -1,13 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { jsonRes } from '@/services/backend/response'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts index 7c04a5f4bcd..dd7650de720 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/sms.ts @@ -1,13 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { sendEmailCodeGuard, filterEmailParams, filterCf } from '@/services/backend/middleware/sms'; -import { enableSms } from '@/services/enable'; -import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindEmailGuard } from '@/services/backend/middleware/oauth'; +import { filterCf, filterEmailParams, sendEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { sendEmailCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts index aef6f20822a..3ded4697279 100644 --- a/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/email/unbind/verify.ts @@ -1,12 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { verifyEmailCodeGuard, filterEmailVerifyParams } from '@/services/backend/middleware/sms'; -import { unbindEmailSvc } from '@/services/backend/svc/bindProvider'; import { ErrorHandler } from '@/services/backend/middleware/error'; +import { filterEmailVerifyParams, verifyEmailCodeGuard } from '@/services/backend/middleware/sms'; +import { unbindEmailSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/info.ts b/frontend/desktop/src/pages/api/auth/info.ts index 634af869c80..3390bf44440 100644 --- a/frontend/desktop/src/pages/api/auth/info.ts +++ b/frontend/desktop/src/pages/api/auth/info.ts @@ -1,8 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; +import { verifyAccessToken } from '@/services/backend/auth'; import { globalPrisma, prisma } from '@/services/backend/db/init'; +import { jsonRes } from '@/services/backend/response'; +import { + enableEmailSms, + enableGithub, + enableGoogle, + enablePassword, + enablePhoneSms +} from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; import { ProviderType } from 'prisma/global/generated/client'; -import { verifyAccessToken } from '@/services/backend/auth'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -12,36 +19,42 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) code: 401, message: 'invalid token' }); - const [regionData, globalData, realNameInfo, restrictedUser] = await Promise.all([ - prisma.userCr.findUnique({ - where: { - uid: regionUser.userCrUid - } - }), - globalPrisma.user.findUnique({ - where: { - uid: regionUser.userUid - }, - include: { - oauthProvider: { - select: { - providerType: true, - providerId: true + const [regionData, globalData, realNameInfo, enterpriseRealNameInfo, restrictedUser] = + await Promise.all([ + prisma.userCr.findUnique({ + where: { + uid: regionUser.userCrUid + } + }), + globalPrisma.user.findUnique({ + where: { + uid: regionUser.userUid + }, + include: { + oauthProvider: { + select: { + providerType: true, + providerId: true + } } } - } - }), - globalPrisma.userRealNameInfo.findUnique({ - where: { - userUid: regionUser.userUid - } - }), - globalPrisma.restrictedUser.findUnique({ - where: { - userUid: regionUser.userUid - } - }) - ]); + }), + globalPrisma.userRealNameInfo.findUnique({ + where: { + userUid: regionUser.userUid + } + }), + globalPrisma.enterpriseRealNameInfo.findUnique({ + where: { + userUid: regionUser.userUid + } + }), + globalPrisma.restrictedUser.findUnique({ + where: { + userUid: regionUser.userUid + } + }) + ]); if (!regionData || !globalData) return jsonRes(res, { @@ -59,23 +72,45 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) id: string; name: string; realName?: string; + enterpriseVerificationStatus?: string; + enterpriseRealName?: string; userRestrictedLevel?: number; } = { ...globalData, - oauthProvider: globalData.oauthProvider.map((o) => ({ - providerType: o.providerType, - providerId: ( - [ProviderType.PHONE, ProviderType.PASSWORD, ProviderType.EMAIL] as ProviderType[] - ).includes(o.providerType) - ? o.providerId - : '' - })) + oauthProvider: globalData.oauthProvider + .filter((o) => { + if (o.providerType === ProviderType.GOOGLE) { + return enableGoogle(); + } else if (o.providerType === ProviderType.GITHUB) { + return enableGithub(); + } else if (o.providerType === ProviderType.PHONE) { + return enablePhoneSms(); + } else if (o.providerType === ProviderType.EMAIL) { + return enableEmailSms(); + } else if (o.providerType === ProviderType.PASSWORD) { + return enablePassword(); + } + return true; + }) + .map((o) => ({ + providerType: o.providerType, + providerId: ( + [ProviderType.PHONE, ProviderType.PASSWORD, ProviderType.EMAIL] as ProviderType[] + ).includes(o.providerType) + ? o.providerId + : '' + })) }; if (realNameInfo && realNameInfo.isVerified) { info.realName = realNameInfo.realName || undefined; } + if (enterpriseRealNameInfo) { + info.enterpriseVerificationStatus = enterpriseRealNameInfo.verificationStatus || undefined; + info.enterpriseRealName = enterpriseRealNameInfo.enterpriseName || undefined; + } + if (restrictedUser) { info.userRestrictedLevel = restrictedUser.restrictedLevel; } diff --git a/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts b/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts index 568a8639663..e0ad3a47c80 100644 --- a/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts +++ b/frontend/desktop/src/pages/api/auth/namespace/abdicate.ts @@ -1,12 +1,12 @@ +import { verifyAccessToken } from '@/services/backend/auth'; +import { prisma } from '@/services/backend/db/init'; import { jsonRes } from '@/services/backend/response'; import { modifyWorkspaceRole } from '@/services/backend/team'; import { UserRole } from '@/types/team'; -import { NextApiRequest, NextApiResponse } from 'next'; -import { prisma } from '@/services/backend/db/init'; import { retrySerially } from '@/utils/tools'; -import { validate as uuidValidate } from 'uuid'; +import { NextApiRequest, NextApiResponse } from 'next'; import { JoinStatus, Role } from 'prisma/region/generated/client'; -import { verifyAccessToken } from '@/services/backend/auth'; +import { validate as uuidValidate } from 'uuid'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { @@ -45,7 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) // modify K8S await modifyWorkspaceRole({ action: 'Change', - pre_k8s_username: payload.userCrUid, + pre_k8s_username: payload.userCrName, k8s_username: target.userCr.crName, role: UserRole.Owner, workspaceId: target.workspace.id diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts index 04b4dfe2f33..d632cc130ac 100644 --- a/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/bind/sms.ts @@ -1,12 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { sendPhoneCodeGuard, filterPhoneParams, filterCf } from '@/services/backend/middleware/sms'; -import { enableSms } from '@/services/enable'; import { ErrorHandler } from '@/services/backend/middleware/error'; +import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterCf(req, res, async () => { diff --git a/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts index 56c208cc893..f6f11f2c7d5 100644 --- a/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/bind/verify.ts @@ -1,14 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { enableSms } from '@/services/enable'; -import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { bindPhoneSvc } from '@/services/backend/svc/bindProvider'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { bindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { bindPhoneSvc } from '@/services/backend/svc/bindProvider'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts index 5b8e7b4525f..c7de8319f5e 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/newSms.ts @@ -1,15 +1,15 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { - filterPhoneParams, filterCodeUid, + filterPhoneParams, sendNewPhoneCodeGuard } from '@/services/backend/middleware/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, ({ userUid }) => diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts index 30bd0065e35..745a3b75cb4 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/oldSms.ts @@ -1,12 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { sendPhoneCodeGuard, filterPhoneParams } from '@/services/backend/middleware/sms'; -import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts index 822acaa4428..f0bd3240b9c 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyNew.ts @@ -1,18 +1,18 @@ -import next, { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; +import { bindPhoneGuard, unbindPhoneGuard } from '@/services/backend/middleware/oauth'; import { - verifyCodeUidGuard, filterCodeUid, filterPhoneVerifyParams, + verifyCodeUidGuard, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { changePhoneBindingSvc } from '@/services/backend/svc/bindProvider'; -import { ErrorHandler } from '@/services/backend/middleware/error'; -import { bindPhoneGuard, unbindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts index 0cde43b58b1..db8812a4e53 100644 --- a/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts +++ b/frontend/desktop/src/pages/api/auth/phone/changeBinding/verifyOld.ts @@ -1,13 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { jsonRes } from '@/services/backend/response'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { jsonRes } from '@/services/backend/response'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/sms.ts b/frontend/desktop/src/pages/api/auth/phone/sms.ts index c1456aebfad..bbc14a574c5 100644 --- a/frontend/desktop/src/pages/api/auth/phone/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/sms.ts @@ -1,11 +1,11 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterCf(req, res, async () => { diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts index b5aa6c8c757..a3e2bf9b5c3 100644 --- a/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/sms.ts @@ -1,13 +1,13 @@ -import { NextApiRequest, NextApiResponse } from 'next'; import { filterAccessToken } from '@/services/backend/middleware/access'; -import { sendPhoneCodeGuard, filterPhoneParams, filterCf } from '@/services/backend/middleware/sms'; -import { enableSms } from '@/services/enable'; -import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; import { ErrorHandler } from '@/services/backend/middleware/error'; import { unbindPhoneGuard } from '@/services/backend/middleware/oauth'; +import { filterCf, filterPhoneParams, sendPhoneCodeGuard } from '@/services/backend/middleware/sms'; +import { sendPhoneCodeSvc } from '@/services/backend/svc/sms'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken(req, res, async ({ userUid }) => { diff --git a/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts index 10edab96ce8..fd18a1ea7a4 100644 --- a/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/unbind/verify.ts @@ -1,12 +1,12 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; import { filterAccessToken } from '@/services/backend/middleware/access'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { unbindPhoneSvc } from '@/services/backend/svc/bindProvider'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterAccessToken( diff --git a/frontend/desktop/src/pages/api/auth/phone/verify.ts b/frontend/desktop/src/pages/api/auth/phone/verify.ts index 8eee1666e82..cde791744ba 100644 --- a/frontend/desktop/src/pages/api/auth/phone/verify.ts +++ b/frontend/desktop/src/pages/api/auth/phone/verify.ts @@ -1,11 +1,11 @@ -import { NextApiRequest, NextApiResponse } from 'next'; -import { enableSms } from '@/services/enable'; +import { ErrorHandler } from '@/services/backend/middleware/error'; import { filterPhoneVerifyParams, verifyPhoneCodeGuard } from '@/services/backend/middleware/sms'; import { getGlobalTokenByPhoneSvc } from '@/services/backend/svc/access'; -import { ErrorHandler } from '@/services/backend/middleware/error'; +import { enablePhoneSms } from '@/services/enable'; +import { NextApiRequest, NextApiResponse } from 'next'; export default ErrorHandler(async function handler(req: NextApiRequest, res: NextApiResponse) { - if (!enableSms()) { + if (!enablePhoneSms()) { throw new Error('SMS is not enabled'); } await filterPhoneVerifyParams( diff --git a/frontend/desktop/src/pages/api/desktop/getResource.ts b/frontend/desktop/src/pages/api/desktop/getResource.ts index f8dc202a7f2..8e569580b53 100644 --- a/frontend/desktop/src/pages/api/desktop/getResource.ts +++ b/frontend/desktop/src/pages/api/desktop/getResource.ts @@ -27,14 +27,15 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) for (const pod of result.body.items) { if (pod.status?.phase === 'Succeeded') continue; + if (!pod?.spec) continue; + totalPodCount++; - if (pod.status?.phase === 'Running') { - runningPodCount++; - } - if (!pod?.spec) continue; + if (pod.status?.phase !== 'Running') continue; + + runningPodCount++; - for (const container of pod.spec.containers) { + for (const container of pod?.spec.containers) { if (!container?.resources) continue; const limits = container?.resources.limits as { cpu: string; diff --git a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts index b375b4b9c8c..fea71d05a62 100644 --- a/frontend/desktop/src/pages/api/platform/getAuthConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getAuthConfig.ts @@ -29,19 +29,22 @@ function genResAuthClientConfig(conf: AuthConfigType) { enabled: !!conf.idp.password?.enabled }, sms: { - enabled: !!conf.idp.sms?.enabled + enabled: !!conf.idp.sms?.ali?.enabled }, github: { enabled: !!conf.idp.github?.enabled, + proxyAddress: conf.idp.github?.proxyAddress || '', clientID: conf.idp.github?.clientID || '' }, wechat: { enabled: !!conf.idp.wechat?.enabled, - clientID: conf.idp.wechat?.clientID || '' + clientID: conf.idp.wechat?.clientID || '', + proxyAddress: conf.idp.wechat?.proxyAddress || '' }, google: { enabled: !!conf.idp.google?.enabled, - clientID: conf.idp.google?.clientID || '' + clientID: conf.idp.google?.clientID || '', + proxyAddress: conf.idp.google?.proxyAddress || '' }, oauth2: { enabled: !!conf.idp.oauth2?.enabled, @@ -49,10 +52,10 @@ function genResAuthClientConfig(conf: AuthConfigType) { clientID: conf.idp.oauth2?.clientID || '', authURL: conf.idp.oauth2?.authURL || '', tokenURL: conf.idp.oauth2?.tokenURL || '', - userInfoURL: conf.idp.oauth2?.userInfoURL || '' + userInfoURL: conf.idp.oauth2?.userInfoURL || '', + proxyAddress: conf.idp.oauth2?.proxyAddress || '' } }, - proxyAddress: conf.proxyAddress || '', hasBaiduToken: !!conf.baiduToken, billingToken: '' }; @@ -63,7 +66,7 @@ export async function getAuthClientConfig(): Promise { try { if (process.env.NODE_ENV === 'development' || !global.AppConfig) { const filename = - process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; + process.env.NODE_ENV === 'development' ? 'data/config.local.yaml' : '/app/data/config.yaml'; global.AppConfig = yaml.load(readFileSync(filename, 'utf-8')) as AppConfigType; } return genResAuthClientConfig(global.AppConfig.desktop.auth); diff --git a/frontend/desktop/src/pages/api/platform/getCloudConfig.ts b/frontend/desktop/src/pages/api/platform/getCloudConfig.ts index 17eb1613e03..ab8baf4c9a4 100644 --- a/frontend/desktop/src/pages/api/platform/getCloudConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getCloudConfig.ts @@ -25,7 +25,7 @@ export async function getCloudConfig(): Promise { if (!global.AppConfig) { const filename = process.env.NODE_ENV === 'development' - ? process.env.CONFIG_PATH || 'data/config.yaml.local' + ? process.env.CONFIG_PATH || 'data/config.local.yaml' : '/app/data/config.yaml'; global.AppConfig = yaml.load(readFileSync(filename, 'utf-8')) as AppConfigType; } diff --git a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts index d9302c3ace0..1de2dd5b796 100644 --- a/frontend/desktop/src/pages/api/platform/getCommonConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getCommonConfig.ts @@ -18,17 +18,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) } function genResCommonClientConfig(common: CommonConfigType): CommonClientConfigType { return { + enterpriseRealNameAuthEnabled: !!common.enterpriseRealNameAuthEnabled, realNameAuthEnabled: !!common.realNameAuthEnabled, guideEnabled: !!common.guideEnabled, rechargeEnabled: !!common.rechargeEnabled, - cfSiteKey: common.cfSiteKey || '' + cfSiteKey: common.cfSiteKey || '', + enterpriseSupportingMaterials: common.enterpriseSupportingMaterials || '' }; } export async function getCommonClientConfig(): Promise { try { if (!global.AppConfig) { const filename = - process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; + process.env.NODE_ENV === 'development' ? 'data/config.local.yaml' : '/app/data/config.yaml'; global.AppConfig = yaml.load(readFileSync(filename, 'utf-8')) as AppConfigType; } return genResCommonClientConfig(global.AppConfig.common); diff --git a/frontend/desktop/src/pages/api/platform/getLayoutConfig.ts b/frontend/desktop/src/pages/api/platform/getLayoutConfig.ts index d871a0cc1ce..28f77d7c4f7 100644 --- a/frontend/desktop/src/pages/api/platform/getLayoutConfig.ts +++ b/frontend/desktop/src/pages/api/platform/getLayoutConfig.ts @@ -16,7 +16,7 @@ export async function getLayoutConfig(): Promise { try { if (!global.AppConfig) { const filename = - process.env.NODE_ENV === 'development' ? 'data/config.yaml.local' : '/app/data/config.yaml'; + process.env.NODE_ENV === 'development' ? 'data/config.local.yaml' : '/app/data/config.yaml'; global.AppConfig = yaml.load(readFileSync(filename, 'utf-8')) as AppConfigType; } return global.AppConfig.desktop.layout || DefaultLayoutConfig; diff --git a/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts b/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts index 8949311f7aa..d1fe1de0d68 100644 --- a/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts +++ b/frontend/desktop/src/pages/api/v1alpha/account/getAccount.ts @@ -1,5 +1,5 @@ -import { AccountMeta } from '@/pages/api/account/getAccount'; import { jsonRes } from '@/services/backend/response'; +import { AccountMeta } from '@/types'; import type { NextApiRequest, NextApiResponse } from 'next'; import { initK8s } from 'sealos-desktop-sdk/service'; diff --git a/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts b/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts index 7f0e7b83ae6..117da5086d2 100644 --- a/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts +++ b/frontend/desktop/src/pages/api/v1alpha/account/updateGuide.ts @@ -1,9 +1,9 @@ import { K8sApiDefault } from '@/services/backend/kubernetes/admin'; import { UpdateCRD } from '@/services/backend/kubernetes/user'; import { jsonRes } from '@/services/backend/response'; +import { AccountMeta } from '@/types'; import type { NextApiRequest, NextApiResponse } from 'next'; import { initK8s } from 'sealos-desktop-sdk/service'; -import { AccountMeta } from '@/pages/api/account/getAccount'; export type UpdateUserGuideParams = { activityType: 'beginner-guide'; diff --git a/frontend/desktop/src/pages/index.tsx b/frontend/desktop/src/pages/index.tsx index cb125e75d70..e90c17333ea 100644 --- a/frontend/desktop/src/pages/index.tsx +++ b/frontend/desktop/src/pages/index.tsx @@ -3,6 +3,7 @@ import DesktopContent from '@/components/desktop_content'; import useAppStore from '@/stores/app'; import useCallbackStore from '@/stores/callback'; import { useConfigStore } from '@/stores/config'; +import { useDesktopConfigStore } from '@/stores/desktopConfig'; import useSessionStore from '@/stores/session'; import { SemData } from '@/types/sem'; import { NSType } from '@/types/team'; @@ -39,6 +40,8 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str const { session } = useSessionStore(); const { layoutConfig } = useConfigStore(); const { workspaceInviteCode, setWorkspaceInviteCode } = useCallbackStore(); + const { setCanShowGuide } = useDesktopConfigStore(); + useEffect(() => { colorMode === 'dark' ? toggleColorMode() : null; }, [colorMode, toggleColorMode]); @@ -127,6 +130,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str let appkey = ''; let appRoute = ''; if (!state.autolaunch) { + setCanShowGuide(true); const result = parseOpenappQuery((query?.openapp as string) || ''); appQuery = result.appQuery; appkey = result.appkey; @@ -141,6 +145,7 @@ export default function Home({ sealos_cloud_domain }: { sealos_cloud_domain: str } const app = state.installedApps.find((item) => item.key === appkey); if (!app) return; + setCanShowGuide(false); state.openApp(app, { raw: appQuery, pathname: appRoute }).then(() => { state.cancelAutoLaunch(); }); diff --git a/frontend/desktop/src/services/backend/auth.ts b/frontend/desktop/src/services/backend/auth.ts index 1c010924ac2..db660550487 100644 --- a/frontend/desktop/src/services/backend/auth.ts +++ b/frontend/desktop/src/services/backend/auth.ts @@ -25,6 +25,7 @@ const verifyToken = async (header: IncomingHttpHeaders) => { return null; } }; + export const verifyAccessToken = async (header: IncomingHttpHeaders) => verifyToken(header).then( (payload) => { @@ -36,6 +37,7 @@ export const verifyAccessToken = async (header: IncomingHttpHeaders) => }, (err) => null ); + export const verifyAuthenticationToken = async (header: IncomingHttpHeaders) => { try { if (!header?.authorization) { @@ -63,6 +65,19 @@ export const verifyJWT = (token?: string, secret? }); }); +export const verifyAppToken = async (header: IncomingHttpHeaders) => { + try { + if (!header?.authorization) { + throw new Error('缺少凭证'); + } + const token = decodeURIComponent(header.authorization); + const payload = await verifyJWT(token, internalJwtSecret()); + return payload; + } catch (err) { + return null; + } +}; + export const generateBillingToken = (props: BillingTokenPayload) => sign(props, internalJwtSecret(), { expiresIn: '3600000' }); export const generateAccessToken = (props: AccessTokenPayload) => diff --git a/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts index 82832f8643b..51a1e99707b 100644 --- a/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts +++ b/frontend/desktop/src/services/backend/cronjob/mergeUserCr.ts @@ -1,13 +1,13 @@ -import { globalPrisma, prisma } from '../db/init'; -import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; -import { JoinStatus } from 'prisma/region/generated/client'; -import { getBillingUrl, getCvmUrl, getRegionUid, getWorkorderUrl } from '@/services/enable'; import { CronJobStatus } from '@/services/backend/cronjob/index'; import { getUserKubeconfigNotPatch } from '@/services/backend/kubernetes/admin'; import { mergeUserModifyBinding, mergeUserWorkspaceRole } from '@/services/backend/team'; -import axios from 'axios'; -import { generateCronJobToken } from '../auth'; +import { getBillingUrl, getCvmUrl, getRegionUid, getWorkorderUrl } from '@/services/enable'; import { MergeUserEvent } from '@/types/db/event'; +import axios from 'axios'; +import { TransactionStatus, TransactionType } from 'prisma/global/generated/client'; +import { JoinStatus } from 'prisma/region/generated/client'; +import { generateBillingToken, generateCronJobToken } from '../auth'; +import { globalPrisma, prisma } from '../db/init'; /** * | | user is exist | user is not exist | @@ -190,13 +190,24 @@ export class MergeUserCrJob implements CronJobStatus { const kubeConfig = await getUserKubeconfigNotPatch(finalUserCr.crName); if (!kubeConfig) throw Error('the kubeconfig for ' + finalUserCr.crName + ' is not found'); const [transferResult, workorderResult, cvmResult] = await Promise.all([ - axios.post(billingUrl, { - kubeConfig, - owner: finalUserCr.crName, - userid: mergeUser.id, - toUser: user.id, - transferAll: true - }), + axios.post( + billingUrl, + { + userid: mergeUser.id, + toUser: user.id, + transferAll: true + }, + { + headers: { + Authorization: + 'Bearer ' + + generateBillingToken({ + userUid: mergeUser.uid, + userId: mergeUser.id + }) + } + } + ), axios.post(workorderUrl, { token: generateCronJobToken({ userUid: user.id, diff --git a/frontend/desktop/src/services/backend/globalAuth.ts b/frontend/desktop/src/services/backend/globalAuth.ts index 6878b0616c9..d746e259823 100644 --- a/frontend/desktop/src/services/backend/globalAuth.ts +++ b/frontend/desktop/src/services/backend/globalAuth.ts @@ -5,9 +5,20 @@ import { AuthConfigType } from '@/types'; import { SemData } from '@/types/sem'; import { hashPassword } from '@/utils/crypto'; import { nanoid } from 'nanoid'; -import { ProviderType, User, UserStatus } from 'prisma/global/generated/client'; +import { + PrismaClient, + ProviderType, + TaskStatus, + User, + UserStatus +} from 'prisma/global/generated/client'; import { enableSignUp } from '../enable'; +type TransactionClient = Omit< + PrismaClient, + '$connect' | '$disconnect' | '$on' | '$transaction' | '$use' | '$extends' +>; + async function signIn({ provider, id }: { provider: ProviderType; id: string }) { const userProvider = await globalPrisma.oauthProvider.findUnique({ where: { @@ -21,6 +32,9 @@ async function signIn({ provider, id }: { provider: ProviderType; id: string }) } }); if (!userProvider) return null; + + await checkDeductionBalanceAndCreateTasks(userProvider.user.uid); + return { user: userProvider.user }; @@ -82,11 +96,65 @@ export async function signInByPassword({ id, password }: { id: string; password: } }); if (!userProvider) return null; + + await checkDeductionBalanceAndCreateTasks(userProvider.user.uid); + return { user: userProvider.user }; } +/** + * Checks the deduction balance of a user and creates new tasks if the balance is zero. + * + * @param {string} userUid - The unique identifier of the user. + */ +async function checkDeductionBalanceAndCreateTasks(userUid: string) { + const account = await globalPrisma.account.findUnique({ + where: { userUid } + }); + + // Check if the account exists, the deduction balance is not null, and the balance is zero. + if ( + account && + account.deduction_balance !== null && + account.deduction_balance.toString() === '0' + ) { + const userTasks = await globalPrisma.userTask.findFirst({ + where: { userUid } + }); + + // If no user tasks are found, create new tasks for the user. + if (!userTasks) { + await globalPrisma.$transaction(async (tx) => { + await createNewUserTasks(tx, userUid); + }); + } + } +} + +// Assign tasks to newly registered users +async function createNewUserTasks(tx: TransactionClient, userUid: string) { + const newUserTasks = await tx.task.findMany({ + where: { + isNewUserTask: true, + isActive: true + } + }); + + for (const task of newUserTasks) { + await tx.userTask.create({ + data: { + userUid, + taskId: task.id, + status: TaskStatus.NOT_COMPLETED, + rewardStatus: task.taskType === 'DESKTOP' ? TaskStatus.COMPLETED : TaskStatus.NOT_COMPLETED, + completedAt: new Date(0) + } + }); + } +} + async function signUp({ provider, id, @@ -128,6 +196,8 @@ async function signUp({ }); } + await createNewUserTasks(tx, user.uid); + return { user }; }); @@ -181,6 +251,8 @@ export async function signUpByPassword({ }); } + await createNewUserTasks(tx, user.uid); + return { user }; }); @@ -244,6 +316,7 @@ export const getGlobalToken = async ({ } } }); + if (provider === ProviderType.PASSWORD) { if (!password) { return null; diff --git a/frontend/desktop/src/services/backend/svc/mergeUser.ts b/frontend/desktop/src/services/backend/svc/mergeUser.ts index d49403ad982..3a413ccda38 100644 --- a/frontend/desktop/src/services/backend/svc/mergeUser.ts +++ b/frontend/desktop/src/services/backend/svc/mergeUser.ts @@ -10,6 +10,7 @@ export const mergeUserSvc = (userUid: string, mergeUserUid: string) => async (res: NextApiResponse) => { const user = await globalPrisma.user.findUnique({ where: { + status: UserStatus.NORMAL_USER, uid: userUid }, include: { diff --git a/frontend/desktop/src/services/enable.ts b/frontend/desktop/src/services/enable.ts index 9b25892a39d..91f700d9a00 100644 --- a/frontend/desktop/src/services/enable.ts +++ b/frontend/desktop/src/services/enable.ts @@ -1,8 +1,11 @@ // for service export const enableRealNameAuth = () => global.AppConfig.common.realNameAuthEnabled || false; +export const enableEnterpriseRealNameAuth = () => + global.AppConfig.common.enterpriseRealNameAuthEnabled || false; export const enablePassword = () => global.AppConfig.desktop.auth.idp.password?.enabled || false; export const enableGithub = () => global.AppConfig.desktop.auth.idp.github?.enabled || false; -export const enableSms = () => global.AppConfig.desktop.auth.idp.sms?.ali?.enabled || false; +export const enablePhoneSms = () => global.AppConfig.desktop.auth.idp.sms?.ali?.enabled || false; +export const enableSms = () => global.AppConfig.desktop.auth.idp.sms?.enabled || false; export const enableEmailSms = () => global.AppConfig.desktop.auth.idp.sms?.email?.enabled || false; export const enableWechat = () => global.AppConfig.desktop.auth.idp.wechat?.enabled || false; export const enableGoogle = () => global.AppConfig.desktop.auth.idp.google?.enabled || false; diff --git a/frontend/desktop/src/stores/desktopConfig.ts b/frontend/desktop/src/stores/desktopConfig.ts index 46d86518265..4c934ed1511 100644 --- a/frontend/desktop/src/stores/desktopConfig.ts +++ b/frontend/desktop/src/stores/desktopConfig.ts @@ -2,7 +2,11 @@ import { create } from 'zustand'; import { persist } from 'zustand/middleware'; import { immer } from 'zustand/middleware/immer'; +type TaskComponentState = 'none' | 'modal' | 'button'; + type State = { + canShowGuide: boolean; + setCanShowGuide: (value: boolean) => void; isAppBar: boolean; isNavbarVisible: boolean; isAnimationEnabled: boolean; @@ -10,6 +14,8 @@ type State = { toggleNavbarVisibility: (forceState?: boolean) => void; temporarilyDisableAnimation: () => void; getTransitionValue: () => string; + taskComponentState: TaskComponentState; + setTaskComponentState: (state: TaskComponentState) => void; }; export const useDesktopConfigStore = create()( @@ -18,6 +24,13 @@ export const useDesktopConfigStore = create()( isAppBar: true, isNavbarVisible: true, isAnimationEnabled: true, + taskComponentState: 'none', + canShowGuide: false, + setCanShowGuide(value) { + set((state) => { + state.canShowGuide = value; + }); + }, toggleShape() { set((state) => { state.isAppBar = !state.isAppBar; @@ -42,6 +55,11 @@ export const useDesktopConfigStore = create()( return get().isAnimationEnabled ? 'transform 200ms ease-in-out, opacity 200ms ease-in-out' : 'none'; + }, + setTaskComponentState(s) { + set((state) => { + state.taskComponentState = s; + }); } })), { diff --git a/frontend/desktop/src/types/crd.ts b/frontend/desktop/src/types/crd.ts index 3d1b57626bf..29dfc31595e 100644 --- a/frontend/desktop/src/types/crd.ts +++ b/frontend/desktop/src/types/crd.ts @@ -13,6 +13,7 @@ export const userCRD = { Version: 'v1', Resource: 'users' }; + export type UserCR = { apiVersion: 'user.sealos.io/v1'; kind: 'User'; @@ -62,6 +63,7 @@ export type StatusCR = { }; code: 404; }; + export type TAppCR = { apiVersion: 'app.sealos.io/v1'; kind: 'App'; @@ -131,9 +133,16 @@ export type TNotification = { desktopPopup: boolean; i18n: { [key in string]: { - from: string; + from: string; // Debt-System Active-System message: string; title: string; }; }; }; + +export const AccountMeta: CRDMeta = { + group: 'account.sealos.io', + version: 'v1', + namespace: 'sealos-system', + plural: 'accounts' +}; diff --git a/frontend/desktop/src/types/session.ts b/frontend/desktop/src/types/session.ts index b80fd6f4d31..043a45bae71 100644 --- a/frontend/desktop/src/types/session.ts +++ b/frontend/desktop/src/types/session.ts @@ -8,6 +8,8 @@ export type OAuthToken = { export type UserInfo = { readonly userRestrictedLevel?: number; readonly realName?: string; + readonly enterpriseVerificationStatus?: string; + readonly enterpriseRealName?: string; readonly k8s_username: string; readonly name: string; readonly avatar: string; diff --git a/frontend/desktop/src/types/system.ts b/frontend/desktop/src/types/system.ts index 5433c5109a5..5f237ff2355 100644 --- a/frontend/desktop/src/types/system.ts +++ b/frontend/desktop/src/types/system.ts @@ -8,6 +8,8 @@ export type CloudConfigType = { }; export type CommonConfigType = { + enterpriseRealNameAuthEnabled: boolean; + enterpriseSupportingMaterials: string; realNameAuthEnabled: boolean; guideEnabled: boolean; apiEnabled: boolean; @@ -71,7 +73,6 @@ export type LayoutConfigType = { export type AuthConfigType = { billingToken?: string; - proxyAddress?: string; callbackURL: string; signUpEnabled?: boolean; baiduToken?: string; @@ -92,19 +93,32 @@ export type AuthConfigType = { }; github?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; wechat?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; google?: { enabled: boolean; + proxyAddress?: string; clientID: string; clientSecret?: string; }; + oauth2?: { + enabled: boolean; + callbackURL: string; + clientID: string; + proxyAddress?: string; + clientSecret?: string; + authURL: string; + tokenURL: string; + userInfoURL: string; + }; sms?: { enabled: boolean; ali?: { @@ -123,15 +137,6 @@ export type AuthConfigType = { password: string; }; }; - oauth2?: { - enabled: boolean; - callbackURL: string; - clientID: string; - clientSecret?: string; - authURL: string; - tokenURL: string; - userInfoURL: string; - }; }; }; @@ -173,12 +178,24 @@ export type DesktopConfigType = { }; }; +export type RealNameOSSConfigType = { + accessKey: string; + accessKeySecret: string; + endpoint: string; + ssl?: boolean; + port?: number; + realNameBucket: string; + enterpriseRealNameBucket: string; +}; + export type AppConfigType = { cloud: CloudConfigType; common: CommonConfigType; database: DatabaseConfigType; desktop: DesktopConfigType; + realNameOSS: RealNameOSSConfigType; }; + export type AppClientConfigType = { cloud: CloudConfigType; common: CommonClientConfigType; @@ -186,6 +203,8 @@ export type AppClientConfigType = { }; export const DefaultCommonClientConfig: CommonClientConfigType = { + enterpriseRealNameAuthEnabled: false, + enterpriseSupportingMaterials: '', realNameAuthEnabled: false, guideEnabled: false, rechargeEnabled: false, @@ -239,15 +258,18 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { }, github: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, wechat: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, google: { enabled: false, - clientID: '' + clientID: '', + proxyAddress: '' }, sms: { enabled: false @@ -258,10 +280,10 @@ export const DefaultAuthClientConfig: AuthClientConfigType = { clientID: '', authURL: '', tokenURL: '', - userInfoURL: '' + userInfoURL: '', + proxyAddress: '' } }, - proxyAddress: '', billingToken: '' }; diff --git a/frontend/desktop/src/types/task.ts b/frontend/desktop/src/types/task.ts new file mode 100644 index 00000000000..b0d1b04f02d --- /dev/null +++ b/frontend/desktop/src/types/task.ts @@ -0,0 +1,12 @@ +import { TaskType } from 'prisma/global/generated/client'; + +export type UserTask = { + id: string; + title: Record; + description: string; + reward: string; + order: number; + taskType: TaskType; + isCompleted: boolean; + completedAt: string; +}; diff --git a/frontend/desktop/src/types/team.ts b/frontend/desktop/src/types/team.ts index 1447cfe99b5..211c46bd6dd 100644 --- a/frontend/desktop/src/types/team.ts +++ b/frontend/desktop/src/types/team.ts @@ -1,3 +1,4 @@ +import { userSystemNamespace } from '@/constants/account'; import { UUID, createHash } from 'crypto'; import * as yaml from 'js-yaml'; export type RoleAction = 'Grant' | 'Deprive' | 'Change' | 'Create' | 'Modify'; @@ -22,9 +23,10 @@ type CRD = { kind: 'Operationrequest'; metadata: { name: string; - namespace: string; + namespace: typeof userSystemNamespace; }; spec: { + namespace: string; user: string; action: 'Deprive' | 'Grant' | 'Update'; role: RoleType; @@ -38,9 +40,10 @@ export const generateRequestCrd = ({ kind: 'Operationrequest', metadata: { name: props.name, - namespace: props.namespace + namespace: userSystemNamespace }, spec: { + namespace: props.namespace, user: props.user, action: props.action, role: props.role @@ -58,6 +61,7 @@ type DeleteCRD = { kind: 'DeleteRequest'; metadata: { name: string; + namespace: typeof userSystemNamespace; }; spec: { user: string; @@ -71,7 +75,8 @@ export const deleteRequestCrd = (props: DeleteCRD['spec']) => { apiVersion: 'user.sealos.io/v1', kind: 'DeleteRequest', metadata: { - name + name, + namespace: userSystemNamespace }, spec: { user: props.user diff --git a/frontend/desktop/src/utils/sessionConfig.ts b/frontend/desktop/src/utils/sessionConfig.ts index bc9c47b5aac..c1c44563f6b 100644 --- a/frontend/desktop/src/utils/sessionConfig.ts +++ b/frontend/desktop/src/utils/sessionConfig.ts @@ -22,6 +22,8 @@ export const sessionConfig = async ({ user: { userRestrictedLevel: infoData.data?.info.userRestrictedLevel || undefined, realName: infoData.data?.info.realName || undefined, + enterpriseVerificationStatus: infoData.data?.info.enterpriseVerificationStatus || undefined, + enterpriseRealName: infoData.data?.info.enterpriseRealName || undefined, k8s_username: payload.userCrName, name: infoData.data?.info.nickname || '', avatar: infoData.data?.info.avatarUri || '', diff --git a/frontend/packages/driver/src/overlay.ts b/frontend/packages/driver/src/overlay.ts index 682153782ea..1f8b8d65b1c 100644 --- a/frontend/packages/driver/src/overlay.ts +++ b/frontend/packages/driver/src/overlay.ts @@ -111,47 +111,8 @@ function mountOverlay(stagePosition: StageDefinition) { document.body.appendChild(skipButton); - let countdown = 5; - let timeoutId: any; - let isButtonDisabled = true; - - const updateButtonText = () => { - skipButton.innerText = `${getConfig('overlaySkipButton') || ''} (${countdown}s)`; - }; - - const countdownInterval = setInterval(() => { - countdown -= 1; - updateButtonText(); - }, 1000); - - timeoutId = setTimeout(() => { - console.log('Auto skipping after 5 seconds'); - clearInterval(countdownInterval); - enableButton(); - skipButton.innerText = getConfig('overlaySkipButton') || ''; - }, 5000); - - updateButtonText(); - - function disableButton() { - isButtonDisabled = true; - skipButton.style.pointerEvents = 'none'; - skipButton.style.opacity = '0.5'; - } - - function enableButton() { - isButtonDisabled = false; - skipButton.style.pointerEvents = 'auto'; - skipButton.style.opacity = '1'; - } - onDriverClick(skipButton, () => { - if (!isButtonDisabled) { - disableButton(); - clearInterval(countdownInterval); - clearTimeout(timeoutId); - emit('skipButtonClick'); - } + emit('skipButtonClick'); }); setState('__overlaySkipBtn', skipButton); diff --git a/frontend/packages/ui/src/components/icons/NotificationIcon.tsx b/frontend/packages/ui/src/components/icons/NotificationIcon.tsx index f9ef5a22227..7e388bea317 100644 --- a/frontend/packages/ui/src/components/icons/NotificationIcon.tsx +++ b/frontend/packages/ui/src/components/icons/NotificationIcon.tsx @@ -7,13 +7,13 @@ export default function NotificationIcon(props: IconProps) { width="21px" height="20px" viewBox="0 0 21 20" - fill="none" + {...props} > ); diff --git a/frontend/packages/ui/src/theme/colors.ts b/frontend/packages/ui/src/theme/colors.ts index 0857f17af1e..d53d379d28a 100644 --- a/frontend/packages/ui/src/theme/colors.ts +++ b/frontend/packages/ui/src/theme/colors.ts @@ -72,6 +72,10 @@ const baseColors = { 800: '#005B9C', 900: '#004B82' }, + royalBlue: { + 100: '#E1EAFF', + 700: '#2B5FD9' + }, yellow: { 25: '#FFFDFA', 50: '#FFFAEB', diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 00c0dfd2852..db919018e26 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -135,6 +135,9 @@ importers: eslint-config-next: specifier: 13.3.0 version: 13.3.0(eslint@8.38.0)(typescript@5.2.2) + formidable: + specifier: ^3.5.1 + version: 3.5.1 framer-motion: specifier: ^10.16.4 version: 10.16.5(react-dom@18.2.0)(react@18.2.0) @@ -204,6 +207,9 @@ importers: react-draggable: specifier: ^4.4.6 version: 4.4.6(react-dom@18.2.0)(react@18.2.0) + react-dropzone: + specifier: ^14.2.3 + version: 14.2.3(react@18.2.0) react-hook-form: specifier: ^7.46.2 version: 7.48.2(react@18.2.0) @@ -241,6 +247,9 @@ importers: '@testing-library/react': specifier: ^14.0.0 version: 14.1.2(react-dom@18.2.0)(react@18.2.0) + '@types/formidable': + specifier: ^3.4.5 + version: 3.4.5 '@types/jest': specifier: ^29.5.10 version: 29.5.10 @@ -1191,6 +1200,9 @@ importers: '@next/font': specifier: 13.1.6 version: 13.1.6 + '@sealos/driver': + specifier: workspace:^ + version: link:../../packages/driver '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui @@ -1360,6 +1372,9 @@ importers: '@tanstack/react-query': specifier: ^4.35.3 version: 4.36.1(react-dom@18.2.0)(react@18.2.0) + '@types/jsonwebtoken': + specifier: ^9.0.3 + version: 9.0.5 axios: specifier: ^1.7.3 version: 1.7.3 @@ -1393,6 +1408,9 @@ importers: js-yaml: specifier: ^4.1.0 version: 4.1.0 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 jszip: specifier: ^3.10.1 version: 3.10.1 @@ -2163,6 +2181,9 @@ importers: '@replit/codemirror-vscode-keymap': specifier: ^6.0.2 version: 6.0.2(@codemirror/autocomplete@6.11.1)(@codemirror/commands@6.3.2)(@codemirror/language@6.9.3)(@codemirror/lint@6.4.2)(@codemirror/search@6.5.5)(@codemirror/state@6.3.2)(@codemirror/view@6.22.1) + '@sealos/driver': + specifier: workspace:^ + version: link:../../packages/driver '@sealos/ui': specifier: workspace:^ version: link:../../packages/ui diff --git a/frontend/providers/applaunchpad/.env.template b/frontend/providers/applaunchpad/.env.template index a27352112d3..3285ba37b23 100644 --- a/frontend/providers/applaunchpad/.env.template +++ b/frontend/providers/applaunchpad/.env.template @@ -1,8 +1 @@ -NEXT_PUBLIC_MOCK_USER= -SEALOS_DOMAIN="cloud.sealos.io" -DOMAIN_PORT= -FASTGPT_KEY= -CURRENCY= -MONITOR_URL= -INGRESS_SECRET= -GUIDE_ENABLED= \ No newline at end of file +NEXT_PUBLIC_MOCK_USER= \ No newline at end of file diff --git a/frontend/providers/applaunchpad/data/config.yaml b/frontend/providers/applaunchpad/data/config.yaml index 13db2236905..f7ab8b485c6 100644 --- a/frontend/providers/applaunchpad/data/config.yaml +++ b/frontend/providers/applaunchpad/data/config.yaml @@ -2,19 +2,21 @@ cloud: domain: 127.0.0.1.nip.io desktopDomain: 127.0.0.1.nip.io port: "" - userDomain: - - 127.0.0.1.nip.io + userDomains: + - name: 127.0.0.1.nip.io + secretName: wildcard-cert common: guideEnabled: false apiEnabled: false launchpad: - ingressTlsSecretName: wildcard-cert eventAnalyze: enabled: false fastGPTKey: "" components: monitor: url: http://launchpad-monitor.sealos.svc.cluster.local:8428 + billing: + url: "http://account-service.account-system.svc:2333" appResourceFormSliderConfig: default: cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000] diff --git a/frontend/providers/applaunchpad/deploy/Kubefile b/frontend/providers/applaunchpad/deploy/Kubefile index e052ade7ab7..bc58eb95ca7 100644 --- a/frontend/providers/applaunchpad/deploy/Kubefile +++ b/frontend/providers/applaunchpad/deploy/Kubefile @@ -9,7 +9,8 @@ ENV cloudDomain="127.0.0.1.nip.io" ENV cloudPort="" ENV certSecretName="wildcard-cert" -ENV ingressTlsSecretName="wildcard-cert" + ENV monitorUrl="http://launchpad-monitor.sealos.svc.cluster.local:8428" +ENV billingUrl="http://account-service.account-system.svc:2333" CMD ["kubectl apply -f manifests"] diff --git a/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl b/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl index 78793ae6eb3..4df46f43427 100644 --- a/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl +++ b/frontend/providers/applaunchpad/deploy/manifests/deploy.yaml.tmpl @@ -15,17 +15,22 @@ data: cloud: domain: {{ .cloudDomain }} port: {{ if .cloudPort }}:{{ .cloudPort }}{{ end }} + desktopDomain: {{ .cloudDomain }} + userDomains: + - name: {{ .cloudDomain }} + secretName: {{ .certSecretName }} common: guideEnabled: false apiEnabled: false launchpad: - ingressTlsSecretName: {{ .ingressTlsSecretName }} eventAnalyze: enabled: false fastGPTKey: "" components: monitor: url: {{ .monitorUrl }} + billing: + url: {{ .billingUrl }} appResourceFormSliderConfig: default: cpu: [100, 200, 500, 1000, 2000, 3000, 4000, 8000] diff --git a/frontend/providers/applaunchpad/public/locales/en/common.json b/frontend/providers/applaunchpad/public/locales/en/common.json index d7868afe211..bafd64012a5 100644 --- a/frontend/providers/applaunchpad/public/locales/en/common.json +++ b/frontend/providers/applaunchpad/public/locales/en/common.json @@ -4,7 +4,6 @@ "Add": "Add", "Add Port": "Add Port", "Add volume": "Add Storage", - "Adjust application configuration": "Adjust application configuration", "Advanced Configuration": "Advanced Config", "Age": "Age", "Amount": "Amount", @@ -32,12 +31,11 @@ "Balance": "Balance", "Basic Config": "Basic", "Basic Information": "Basic", - "Can help you deploy any Docker image": "Helps you deploy any Docker image", + "Can help you deploy any Docker image": "Rich image warehouse, supporting any Docker image", "Can not change storage path": "Storage mount path cannot be modified", "Cancel": "Cancel", "capacity": "capacity", "Card": "cards", - "Click here to visit the website": "Click here to open the site", "Click on any shadow to skip": "Click on any shadow to skip", "Click the Deploy Application button": "Click \\\"Deploy Application\\\"", "Cname auth error: customDomain's cname is not equal to publicDomain": "CNAME error. You must configure CNAME to the provided domain before binding.", @@ -99,7 +97,6 @@ "File Value can not empty": "File content is required", "filename": "File Name", "Filename can not empty": "File Name is required", - "First time completion guide benefits": "First-time user bonus", "Fixed instance": "Fixed", "Folder Name": "Folder Name", "gift amount tip": "Top up {{amount}} to get {{gift}} bonus", @@ -188,7 +185,6 @@ "Restarts Num": "Restarts", "Run command": "Command", "Running": "Running", - "Second-level domain name tips": "Public subdomain will be automatically assigned if enabled", "Separated by spaces": "Space separated, e.g.:", "show hidden files": "Show hidden files", "Size": "Size", @@ -266,5 +262,15 @@ "within_1_day": "Within 1 day", "terminated_logs": "Terminated logs", "no_logs_for_now": "No logs for now", - "or": "or" -} \ No newline at end of file + "or": "or", + "guide_deploy_storage": "Massive storage to meet various application needs", + "guide_detail_operate": "Log|Terminal|Details|Restart", + "guide_detail_monitor": "Visual resource monitoring", + "guide_detail_update_button": "Adjust in real time as needed", + "guide_detail_network": "Provide intranet and extranet access addresses and automatically configure SSL certificates", + "first_charge": "First Recharge Discount", + "first_charge_tip": "For some specifications, you can enjoy double the gift amount when you recharge for the first time.", + "gift": "gift", + "balance": "balance", + "guide_deploy_button": "Complete creation and get it now" +} diff --git a/frontend/providers/applaunchpad/public/locales/zh/common.json b/frontend/providers/applaunchpad/public/locales/zh/common.json index 8fb7cb20a65..56d12456d25 100644 --- a/frontend/providers/applaunchpad/public/locales/zh/common.json +++ b/frontend/providers/applaunchpad/public/locales/zh/common.json @@ -4,7 +4,6 @@ "Add": "新增", "Add Port": "添加端口", "Add volume": "新增存储卷", - "Adjust application configuration": "调整应用配置", "Advanced Configuration": "高级配置", "Age": "启动时长", "Amount": "数量", @@ -32,12 +31,11 @@ "Balance": "余额", "Basic Config": "基础配置", "Basic Information": "基本信息", - "Can help you deploy any Docker image": "可以帮助您部署任意 Docker 镜像", + "Can help you deploy any Docker image": "丰富的镜像仓库,支持任意 Docker 镜像", "Can not change storage path": "不允许修改挂载路径", "Cancel": "取消", "capacity": "容量", "Card": "张", - "Click here to visit the website": "点击此处访问网站", "Click on any shadow to skip": "点击任意阴影跳过", "Click the Deploy Application button": "点击「部署应用」按钮", "Cname auth error: customDomain's cname is not equal to publicDomain": "CNAME 校验错误,你需要先 CNAME 到指定域名才能绑定", @@ -99,7 +97,6 @@ "File Value can not empty": "文件值不能为空", "filename": "文件名", "Filename can not empty": "文件名不能为空", - "First time completion guide benefits": "首次完成引导福利", "Fixed instance": "固定实例", "Folder Name": "文件夹名", "gift amount tip": "充值 {{amount}} 赠送 {{gift}} ", @@ -179,7 +176,7 @@ "Public Address": "公网地址", "Real-time Monitoring": "实时监控", "Reboot Success": "重启成功", - "receive": "获得", + "receive": "已获得", "rename": "重命名", "Replicas": "实例数", "Restart": "重启", @@ -188,7 +185,6 @@ "Restarts Num": "重启次数", "Run command": "运行命令", "Running": "运行中", - "Second-level domain name tips": "开启后将自动为您分配一个二级公网域名", "Separated by spaces": "以空格分开,如: ", "show hidden files": "显示隐藏文件", "Size": "文件大小", @@ -266,5 +262,16 @@ "within_1_day": "一天内", "terminated_logs": "中断前", "no_logs_for_now": "暂无日志", - "or": "或" -} \ No newline at end of file + "or": "或", + "guide_deploy_command": "丰富的命令集支持,简化服务器管理", + "guide_deploy_storage": "海量存储,满足各种应用需求", + "guide_detail_monitor": "可视化资源监控", + "guide_detail_operate": "日志|终端|详情|重启", + "guide_detail_update_button": "按需实时调整", + "guide_detail_network": "提供内网和外网访问地址,并自动配置 SSL 证书", + "first_charge": "首充优惠", + "first_charge_tip": "部分规格首次充值可享双倍赠送金额", + "gift": "赠", + "balance": "余额", + "guide_deploy_button": "完成创建,立即获得" +} diff --git a/frontend/providers/applaunchpad/src/api/platform.ts b/frontend/providers/applaunchpad/src/api/platform.ts index e8fe277b932..406773b8256 100644 --- a/frontend/providers/applaunchpad/src/api/platform.ts +++ b/frontend/providers/applaunchpad/src/api/platform.ts @@ -1,9 +1,8 @@ import type { Response as InitDataType } from '@/pages/api/platform/getInitData'; import { GET, POST } from '@/services/request'; -import { EnvResponse } from '@/types'; -import type { AccountCRD, UserQuotaItemType, userPriceType } from '@/types/user'; +import type { UserQuotaItemType, UserTask, userPriceType } from '@/types/user'; +import { getUserSession } from '@/utils/user'; import { AuthCnamePrams } from './params'; -import { UpdateUserGuideParams } from '@/pages/api/guide/updateGuide'; export const getResourcePrice = () => GET('/api/platform/resourcePrice'); @@ -16,12 +15,26 @@ export const getUserQuota = () => export const postAuthCname = (data: AuthCnamePrams) => POST('/api/platform/authCname', data); -export const updateDesktopGuide = (payload: UpdateUserGuideParams) => - POST('/api/guide/updateGuide', payload); - -export const getUserAccount = () => GET('/api/guide/getAccount'); - -export const getPriceBonus = () => GET('/api/guide/getBonus'); +export const getUserTasks = () => + GET<{ needGuide: boolean; task: UserTask }>('/api/guide/getTasks', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); + +export const checkUserTask = () => + GET('/api/guide/checkTask', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); + +export const getPriceBonus = () => + GET<{ amount: number; gift: number }[]>('/api/guide/getBonus', undefined, { + headers: { + Authorization: getUserSession()?.token + } + }); export const checkPermission = (payload: { appName: string }) => GET('/api/platform/checkPermission', payload); diff --git a/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg b/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg new file mode 100644 index 00000000000..865d6307dbb --- /dev/null +++ b/frontend/providers/applaunchpad/src/components/Icon/icons/gift.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/frontend/providers/applaunchpad/src/components/Icon/index.tsx b/frontend/providers/applaunchpad/src/components/Icon/index.tsx index 6f72e383a5e..bf1f410e152 100644 --- a/frontend/providers/applaunchpad/src/components/Icon/index.tsx +++ b/frontend/providers/applaunchpad/src/components/Icon/index.tsx @@ -29,6 +29,7 @@ const map = { noEvents: require('./icons/noEvents.svg').default, warning: require('./icons/warning.svg').default, analyze: require('./icons/analyze.svg').default, + gift: require('./icons/gift.svg').default, terminal: require('./icons/terminal.svg').default, log: require('./icons/log.svg').default, nvidia: require('./icons/gpu/nvidia.svg').default, diff --git a/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx b/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx index 2353e8ac15c..36ec95d5a70 100644 --- a/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useDetailDriver.tsx @@ -1,22 +1,34 @@ -import { getInitData, getPriceBonus, getUserAccount, updateDesktopGuide } from '@/api/platform'; -import { GUIDE_LAUNCHPAD_DETAIL_KEY, GUIDE_LAUNCHPAD_GIFT_KEY } from '@/constants/account'; +import { checkUserTask, getPriceBonus, getUserTasks } from '@/api/platform'; +import MyIcon from '@/components/Icon'; +import { useGuideStore } from '@/store/guide'; import { formatMoney } from '@/utils/tools'; -import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; +import { Center, Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; import { DriveStep, driver } from '@sealos/driver'; +import { SealosCoin } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; import { useEffect, useMemo, useState } from 'react'; import { sealosApp } from 'sealos-desktop-sdk/app'; import { DriverStarIcon } from './useDriver'; -export default function useDriver() { +export default function useDetailDriver() { const { t, i18n } = useTranslation(); - const [showGiftStep, setShowGiftStep] = useState(false); - const [activity, setActivity] = useState({ - balance: 8, - limitDuration: '1', - amount: '8', - giftAmount: '8' - }); + const [reward, setReward] = useState(5); + const { detailCompleted, setDetailCompleted } = useGuideStore(); + + const [rechargeOptions, setRechargeOptions] = useState([ + { amount: 8, gift: 8 }, + { amount: 32, gift: 32 }, + { amount: 128, gift: 128 } + ]); + + const openCostCenterApp = () => { + sealosApp.runEvents('openDesktopApp', { + appKey: 'system-costcenter', + query: { + openRecharge: 'true' + } + }); + }; const PopoverBodyInfo = (props: FlexProps) => { return ( @@ -51,296 +63,250 @@ export default function useDriver() { const baseSteps: DriveStep[] = [ { - element: '.driver-detail-network-public', + element: '.driver-detail-monitor', popover: { - side: 'left', + side: 'bottom', align: 'start', - borderRadius: '12px 12px 0px 12px', + borderRadius: '0px 12px 12px 12px', PopoverBody: ( - - {t('Click here to visit the website')} + + {t('guide_detail_monitor')} - + ) } }, { - element: '.driver-detail-terminal', + element: '.driver-detail-update-button', popover: { - side: 'left', - align: 'center', + side: 'bottom', + align: 'start', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - {t('You can enter the container through the terminal')} + + {t('guide_detail_update_button')} - + ) } }, { - element: '.driver-detail-update-button', + element: '.driver-detail-network', popover: { - side: 'bottom', + side: 'left', align: 'start', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - {t('Adjust application configuration')} + + {t('guide_detail_network')} - + ) } - } - ]; - - const giftStep: DriveStep[] = [ + }, { + element: '.driver-detail-operate', popover: { - borderRadius: '12px 12px 12px 12px', + side: 'left', + align: 'center', + borderRadius: '12px 12px 0px 12px', PopoverBody: ( - - - - {t('You have successfully deployed an application')} - - {t('receive')} - - {activity.balance} - - - - {t('Balance')} - - - - - - - - - - - - - - - - - - {t('First time completion guide benefits')} - - - - {t('gift time tip', { time: activity.limitDuration })} - - - {t('gift amount tip', { - amount: activity.amount, - gift: activity.giftAmount - })} - - - { - console.log('充值'); - driverObj.destroy(); - openCostCenterApp(); - }} - > - {t('Go to recharge')} - - { - driverObj.destroy(); - }} - > - {t('let me think again')} + + + + {t('guide_detail_operate')} + - ), - onPopoverRender: () => { - const svg = driverObj.getState('__overlaySvg'); - if (svg) { - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.style.pointerEvents = 'none'; - } - } - } + ) } } ]; - const driverConfig = useMemo(() => { - return { - disableActiveInteraction: true, - showProgress: false, - allowClose: false, - allowClickMaskNextStep: true, - allowPreviousStep: false, - isShowButtons: false, - allowKeyboardControl: false, - overlaySkipButton: t('skip') || 'skip', - steps: showGiftStep ? [...baseSteps, ...giftStep] : baseSteps, - onDestroyed: () => { - console.log('onDestroyed Detail'); - updateGuideStatus(); - }, - interceptSkipButtonClick: () => { - const skipButton = driverObj.getState('__overlaySkipBtn'); - skipButton?.remove(); - if (driverObj.isLastStep()) { - driverObj.destroy(); - } else { - driverObj.drive(3); - } - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [showGiftStep]); + const giftStep: DriveStep[] = useMemo( + () => [ + { + popover: { + borderRadius: '12px 12px 12px 12px', + PopoverBody: ( + + + + + {t('You have successfully deployed an application')} + + + {t('receive')} + + + {reward} + + {t('Balance')} + + - const driverObj = driver(driverConfig); + + + + {t('first_charge')} + + - const openCostCenterApp = () => { - sealosApp.runEvents('openDesktopApp', { - appKey: 'system-costcenter', - query: { - openRecharge: 'true' + + {rechargeOptions.map((item, index) => ( +
+ + + {item.amount} + + + {t('gift')} + + {item.gift} + +
+ ))} +
+ + { + driverObj.destroy(); + openCostCenterApp(); + }} + > + {t('Go to recharge')} + + { + driverObj.destroy(); + }} + > + {t('let me think again')} + +
+ ) + } } - }); - }; + ], + [rechargeOptions, reward, t] + ); - const updateGuideStatus = () => { - updateDesktopGuide({ - activityType: 'beginner-guide', - phase: 'launchpad', - phasePage: 'detail', - shouldSendGift: false - }).catch((err) => { - console.log(err); - }); + const driverObj = driver({ + disableActiveInteraction: true, + showProgress: false, + allowClose: false, + allowClickMaskNextStep: true, + allowPreviousStep: false, + isShowButtons: false, + allowKeyboardControl: false, + steps: [...baseSteps, ...giftStep], + onDestroyed: () => { + console.log('onDestroyed Detail'); + setDetailCompleted(true); + checkUserTask().then((err) => { + console.log(err); + }); + }, + interceptSkipButtonClick: () => { + driverObj.destroy(); + } + }); + + const startGuide = () => { + driverObj.drive(); }; useEffect(() => { const handleUserGuide = async () => { try { - const { guideEnabled } = await getInitData(); - const userAccount = await getUserAccount(); - - const bonus = await getPriceBonus(); - if (bonus?.data?.activities) { - const strategy = JSON.parse(bonus.data?.activities); - const activity = { - balance: formatMoney( - strategy?.['beginner-guide']?.phases?.launchpad?.giveAmount || 8000000 - ), - limitDuration: strategy?.[ - 'beginner-guide' - ]?.phases?.launchpad?.RechargeDiscount?.limitDuration?.replace('h', ''), - amount: Object.entries( - strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount - )[0][0], - giftAmount: - Object.entries( - strategy?.['beginner-guide']?.phases?.launchpad?.RechargeDiscount?.specialDiscount - )[0][1] + '' - }; - setActivity(activity); - } - - if (guideEnabled && userAccount?.metadata?.annotations) { - const showGiftStep = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_GIFT_KEY]; - const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_DETAIL_KEY]; - if (!isGuided) { - setShowGiftStep(showGiftStep); + const [taskData, bonusData] = await Promise.all([getUserTasks(), getPriceBonus()]); + if (taskData.needGuide && !detailCompleted) { + setReward(formatMoney(Number(taskData.task.reward))); + setRechargeOptions(bonusData); + requestAnimationFrame(() => { startGuide(); - } + }); } } catch (error) { console.log(error); } }; handleUserGuide(); - // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const startGuide = () => { - driverObj.drive(); - }; - - const closeGuide = () => { - driverObj.destroy(); - }; - - return { startGuide, closeGuide }; + return { startGuide }; } diff --git a/frontend/providers/applaunchpad/src/hooks/useDriver.tsx b/frontend/providers/applaunchpad/src/hooks/useDriver.tsx index 85b6b1209ba..fcbd40fe68c 100644 --- a/frontend/providers/applaunchpad/src/hooks/useDriver.tsx +++ b/frontend/providers/applaunchpad/src/hooks/useDriver.tsx @@ -1,77 +1,48 @@ -import { getInitData, getUserAccount, updateDesktopGuide } from '@/api/platform'; -import { GUIDE_LAUNCHPAD_CREATE_KEY } from '@/constants/account'; +import { getUserTasks } from '@/api/platform'; +import { useGuideStore } from '@/store/guide'; +import { formatMoney } from '@/utils/tools'; import { Flex, FlexProps, Icon, Text } from '@chakra-ui/react'; import { driver } from '@sealos/driver'; +import { SealosCoin } from '@sealos/ui'; import { useTranslation } from 'next-i18next'; -import { useEffect, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; export function DriverStarIcon() { return ( - + - - - + + + ); } -export default function useDriver() { +export default function useDriver({ + setIsAdvancedOpen +}: { + setIsAdvancedOpen: (val: boolean) => void; +}) { const { t } = useTranslation(); - const [isGuided, setIsGuided] = useState(true); - - useEffect(() => { - const handleUserGuide = async () => { - try { - const { guideEnabled } = await getInitData(); - const userAccount = await getUserAccount(); - if (guideEnabled && userAccount?.metadata?.annotations) { - const isGuided = !!userAccount.metadata.annotations?.[GUIDE_LAUNCHPAD_CREATE_KEY]; - if (!isGuided) { - startGuide(); - } - setIsGuided(isGuided); - } - } catch (error) { - console.log(error); - } - }; - handleUserGuide(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const updateGuideStatus = () => { - updateDesktopGuide({ - activityType: 'beginner-guide', - phase: 'launchpad', - phasePage: 'create', - shouldSendGift: false - }).catch((err) => { - console.log(err); - }); - }; + const [isGuided, setIsGuided] = useState(false); + const { createCompleted, setCreateCompleted } = useGuideStore(); + const [reward, setReward] = useState(1); const PopoverBodyInfo = (props: FlexProps) => { return ( @@ -112,6 +83,7 @@ export default function useDriver() { isShowButtons: false, allowKeyboardControl: false, overlaySkipButton: t('skip') || 'skip', + disableActiveInteraction: true, steps: [ { element: '.driver-deploy-image', @@ -122,7 +94,7 @@ export default function useDriver() { PopoverBody: ( - + {t('Can help you deploy any Docker image')} @@ -131,16 +103,16 @@ export default function useDriver() { } }, { - element: '.driver-deploy-instance', + element: '.driver-deploy-command', popover: { - side: 'top', - align: 'start', - borderRadius: '12px 12px 12px 0px', + side: 'right', + align: 'center', + borderRadius: '0px 12px 12px 12px', PopoverBody: ( - - {t('Configurable number of instances or automatic horizontal scaling')} + + {t('guide_deploy_command')} @@ -148,7 +120,7 @@ export default function useDriver() { } }, { - element: '.driver-deploy-network-switch', + element: '.driver-deploy-storage', popover: { side: 'top', align: 'start', @@ -156,8 +128,8 @@ export default function useDriver() { PopoverBody: ( - - {t('Second-level domain name tips')} + + {t('guide_deploy_storage')} @@ -171,37 +143,49 @@ export default function useDriver() { align: 'center', borderRadius: '12px 12px 0px 12px', PopoverBody: ( - + - - {t('Click the Deploy Application button')} - + {t('guide_deploy_button')} + {reward} + + {t('balance')} - ), - onPopoverRender: () => { - const svg = driverObj.getState('__overlaySvg'); - if (svg) { - const pathElement = svg.querySelector('path'); - if (pathElement) { - pathElement.style.pointerEvents = 'none'; - } - } - } + ) } } ], onDestroyed: () => { - updateGuideStatus(); + setCreateCompleted(true); } }); - const startGuide = () => { + const startGuide = useCallback(() => { driverObj.drive(); - }; + }, [driverObj]); const closeGuide = () => { driverObj.destroy(); }; - return { driverObj, startGuide, closeGuide, isGuided }; + useEffect(() => { + const handleUserGuide = async () => { + try { + const data = await getUserTasks(); + if (data.needGuide && !createCompleted) { + setReward(formatMoney(Number(data.task.reward))); + setIsAdvancedOpen(true); + setIsGuided(true); + requestAnimationFrame(() => { + startGuide(); + }); + } + } catch (error) { + setIsGuided(false); + } + }; + handleUserGuide(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return { startGuide, closeGuide, isGuided }; } diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts b/frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts similarity index 66% rename from frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts rename to frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts index a20be083632..458be833a9b 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getAccount.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/checkTask.ts @@ -1,4 +1,4 @@ -import { authSession } from '@/services/backend/auth'; +import { authAppToken } from '@/services/backend/auth'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import type { NextApiRequest, NextApiResponse } from 'next'; @@ -6,15 +6,21 @@ import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const kubeconfig = await authSession(req.headers); - const domain = global.AppConfig.cloud.domain; + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: 'token is valid' }); + } + + const domain = global.AppConfig.cloud.desktopDomain; - const response = await fetch(`https://${domain}/api/v1alpha/account/getAccount`, { + const response = await fetch(`https://${domain}/api/account/checkTask`, { method: 'GET', headers: { - Authorization: encodeURIComponent(kubeconfig) + 'Content-Type': 'application/json', + Authorization: token } }); + const result: { code: number; data: any; diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts index 50dc5c8cea4..4ad97b2c826 100644 --- a/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getBonus.ts @@ -1,22 +1,43 @@ -import { authSession } from '@/services/backend/auth'; -import { getK8s } from '@/services/backend/kubernetes'; +import { authAppToken } from '@/services/backend/auth'; import { jsonRes } from '@/services/backend/response'; import { ApiResp } from '@/services/kubernet'; import type { NextApiRequest, NextApiResponse } from 'next'; export default async function handler(req: NextApiRequest, res: NextApiResponse) { try { - if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const { k8sCore, namespace } = await getK8s({ - kubeconfig: await authSession(req.headers) + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: '令牌无效' }); + } + + const url = global.AppConfig.launchpad.components.billing.url; + + const response = await fetch(`${url}/account/v1alpha1/recharge-discount`, { + method: 'POST', + headers: { + Authorization: `Bearer ${token}` + } }); - const result = await k8sCore.readNamespacedConfigMap('recharge-gift', 'sealos'); + const result: { + discount: { + firstRechargeDiscount: Record; + }; + } = await response.json(); + + const rechargeOptions = Object.entries(result.discount.firstRechargeDiscount).map( + ([amount, rate]) => ({ + amount: Number(amount), + gift: Math.floor((Number(amount) * Number(rate)) / 100) + }) + ); jsonRes(res, { - data: result.body + code: 200, + data: rechargeOptions }); } catch (err: any) { + console.log(err); jsonRes(res, { code: 500, error: err diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts b/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts new file mode 100644 index 00000000000..9321febd240 --- /dev/null +++ b/frontend/providers/applaunchpad/src/pages/api/guide/getTasks.ts @@ -0,0 +1,55 @@ +import { authAppToken } from '@/services/backend/auth'; +import { jsonRes } from '@/services/backend/response'; +import { ApiResp } from '@/services/kubernet'; +import { UserTask } from '@/types/user'; +import type { NextApiRequest, NextApiResponse } from 'next'; + +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + try { + if (!global.AppConfig.common.guideEnabled) + return jsonRes(res, { + data: { + needGuide: false + } + }); + + const token = await authAppToken(req.headers); + if (!token) { + return jsonRes(res, { code: 401, message: 'token is valid' }); + } + + const domain = global.AppConfig.cloud.desktopDomain; + const response = await fetch(`https://${domain}/api/account/getTasks`, { + method: 'GET', + headers: { + Authorization: token + } + }); + const result: { + code: number; + data: UserTask[]; + message: string; + } = await response.json(); + + if (result.code !== 200) { + return jsonRes(res, { + code: 500, + message: 'desktop api is err' + }); + } + + const launchpadTask = result.data.find((task) => task.taskType === 'LAUNCHPAD'); + const needGuide = launchpadTask ? !launchpadTask.isCompleted : false; + + jsonRes(res, { + code: 200, + data: { needGuide, task: launchpadTask } + }); + } catch (err: any) { + console.log(err); + jsonRes(res, { + code: 500, + error: err + }); + } +} diff --git a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts b/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts deleted file mode 100644 index 7d46c77c43a..00000000000 --- a/frontend/providers/applaunchpad/src/pages/api/guide/updateGuide.ts +++ /dev/null @@ -1,57 +0,0 @@ -import { authSession } from '@/services/backend/auth'; -import { jsonRes } from '@/services/backend/response'; -import { ApiResp } from '@/services/kubernet'; -import type { NextApiRequest, NextApiResponse } from 'next'; - -export type UpdateUserGuideParams = { - activityType: 'beginner-guide'; - phase: 'launchpad' | 'database' | 'template' | 'terminal'; - phasePage: 'create' | 'detail' | 'index'; - shouldSendGift: boolean; -}; - -export default async function handler(req: NextApiRequest, res: NextApiResponse) { - try { - if (!global.AppConfig.common.guideEnabled) return jsonRes(res, { data: null }); - const { activityType, phase, phasePage, shouldSendGift } = req.body as UpdateUserGuideParams; - - if (!activityType || !phase || !phasePage) - return jsonRes(res, { code: 400, message: 'Bad Request: Invalid parameters' }); - const kubeconfig = await authSession(req.headers); - const domain = global.AppConfig.cloud.domain; - - const payload = { - activityType, - phase, - phasePage, - shouldSendGift - }; - - const response = await fetch(`https://${domain}/api/v1alpha/account/updateGuide`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: encodeURIComponent(kubeconfig) - }, - body: JSON.stringify(payload) - }); - - const result: { - code: number; - data: any; - message: string; - } = await response.json(); - - if (result.code !== 200) { - return jsonRes(res, { code: result.code, message: 'desktop api is err' }); - } else { - return jsonRes(res, { data: result.data }); - } - } catch (err: any) { - console.log(err); - jsonRes(res, { - code: 500, - error: err - }); - } -} diff --git a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts index f48dcbffff6..b3d6be33b70 100644 --- a/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts +++ b/frontend/providers/applaunchpad/src/pages/api/platform/getInitData.ts @@ -10,13 +10,12 @@ import { getGpuNode } from './resourcePrice'; export type Response = { SEALOS_DOMAIN: string; DOMAIN_PORT: string; - INGRESS_SECRET: string; SHOW_EVENT_ANALYZE: boolean; FORM_SLIDER_LIST_CONFIG: FormSliderListType; CURRENCY: Coin; guideEnabled: boolean; fileMangerConfig: FileMangerType; - SEALOS_USER_DOMAIN: string[]; + SEALOS_USER_DOMAINS: { name: string; secretName: string }[]; DESKTOP_DOMAIN: string; }; @@ -24,7 +23,12 @@ export const defaultAppConfig: AppConfigType = { cloud: { domain: 'cloud.sealos.io', port: '', - userDomain: ['cloud.sealos.io'], + userDomains: [ + { + name: 'cloud.sealos.io', + secretName: 'wildcard-cert' + } + ], desktopDomain: 'cloud.sealos.io' }, common: { @@ -33,7 +37,6 @@ export const defaultAppConfig: AppConfigType = { gpuEnabled: false }, launchpad: { - ingressTlsSecretName: 'wildcard-cert', eventAnalyze: { enabled: false, fastGPTKey: '' @@ -83,13 +86,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) data: { SEALOS_DOMAIN: global.AppConfig.cloud.domain, DOMAIN_PORT: global.AppConfig.cloud.port?.toString() || '', - INGRESS_SECRET: global.AppConfig.launchpad.ingressTlsSecretName, SHOW_EVENT_ANALYZE: global.AppConfig.launchpad.eventAnalyze.enabled, FORM_SLIDER_LIST_CONFIG: global.AppConfig.launchpad.appResourceFormSliderConfig, guideEnabled: global.AppConfig.common.guideEnabled, fileMangerConfig: global.AppConfig.launchpad.fileManger, CURRENCY: Coin.shellCoin, - SEALOS_USER_DOMAIN: global.AppConfig.cloud.userDomain || [], + SEALOS_USER_DOMAINS: global.AppConfig.cloud.userDomains || [], DESKTOP_DOMAIN: global.AppConfig.cloud.desktopDomain } }); diff --git a/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts b/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts index dd6ff058b61..39e612a01e1 100644 --- a/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts +++ b/frontend/providers/applaunchpad/src/pages/api/v1alpha/createApp.ts @@ -17,6 +17,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse< kubeconfig: await authSession(req.headers) }); + appForm.networks = appForm.networks.map((network) => ({ + ...network, + domain: global.AppConfig.cloud.domain + })); + const parseYamls = formData2Yamls(appForm); const yamls = parseYamls.map((item) => item.value); diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx index 89f98aae550..d5ad98246bb 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppBaseInfo.tsx @@ -238,21 +238,6 @@ const AppBaseInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { {t(item.label)} - - copyData(item.value)} - cursor={'pointer'} - > - {t(item.value)} - - ))} {/* env */} diff --git a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx index 7b050b6afea..53efbbae50b 100644 --- a/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx +++ b/frontend/providers/applaunchpad/src/pages/app/detail/components/AppMainInfo.tsx @@ -59,6 +59,7 @@ const AppMainInfo = ({ app = MOCK_APP_DETAIL }: { app: AppDetailType }) => { color={'grayModern.600'} fontWeight={'bold'} position={'relative'} + className="driver-detail-monitor" >