From 0d036e7449d991d1b02eb0e20141ed9273ecd5fa Mon Sep 17 00:00:00 2001 From: barnettZQG <576501057@qq.com> Date: Tue, 12 Oct 2021 11:53:24 +0800 Subject: [PATCH] Feat: initialize the Apiserver framework (#2417) * Feat: add kubeapi and mongodb datastore implementation * Style: change kubeapi import code style * Style: change mongodb package import code style * Style: add some comment * Style: change databasePrefix to tableNamePrefix * Chore: install mongodb in unit-test job * Chore: install mongodb in compatibility-test job * Feat: add apiserver e2e test case * Docs: change developer guide doc * Feat: use common.Scheme Co-authored-by: barnettZQG --- .github/workflows/apiserver-test.yaml | 93 + .github/workflows/unit-test.yml | 4 +- Makefile | 15 +- cmd/apiserver/main.go | 7 +- contribute/developer-guide.md | 14 +- docs/apidoc/swagger.json | 2695 +++++++++++++++++ go.mod | 3 + go.sum | 14 +- pkg/apiserver/clients/kubeclient.go | 47 + pkg/apiserver/datastore/datastore.go | 90 +- .../datastore/datastore_suite_test.go | 29 + pkg/apiserver/datastore/datastore_test.go | 60 + pkg/apiserver/datastore/kubeapi/kubeapi.go | 208 +- .../datastore/kubeapi/kubeapi_suite_test.go | 29 + .../datastore/kubeapi/kubeapi_test.go | 167 + pkg/apiserver/datastore/mongodb/mongodb.go | 151 +- .../datastore/mongodb/mongodb_suite_test.go | 40 + .../datastore/mongodb/mongodb_test.go | 136 + pkg/apiserver/model/application.go | 36 + pkg/apiserver/model/model.go | 19 + pkg/apiserver/rest/apis/v1/types.go | 103 +- pkg/apiserver/rest/rest_server.go | 2 +- pkg/apiserver/rest/usecase/application.go | 107 + .../{cluster_usecase.go => cluster.go} | 0 pkg/apiserver/rest/utils/bcode/application.go | 26 + pkg/apiserver/rest/utils/bcode/bcode.go | 35 +- pkg/apiserver/rest/webservice/application.go | 65 +- pkg/apiserver/rest/webservice/cluster.go | 13 +- .../rest/webservice/policy_definition.go | 43 + pkg/apiserver/rest/webservice/validate.go | 42 + .../rest/webservice/validate_test.go | 56 + pkg/apiserver/rest/webservice/webservice.go | 17 +- .../rest/webservice/webservice_suite_test.go | 29 + pkg/apiserver/rest/webservice/workflow.go | 60 + test/e2e-apiserver-test/application_test.go | 57 + test/e2e-apiserver-test/suite_test.go | 98 + 36 files changed, 4478 insertions(+), 132 deletions(-) create mode 100644 .github/workflows/apiserver-test.yaml create mode 100644 docs/apidoc/swagger.json create mode 100644 pkg/apiserver/clients/kubeclient.go create mode 100644 pkg/apiserver/datastore/datastore_suite_test.go create mode 100644 pkg/apiserver/datastore/datastore_test.go create mode 100644 pkg/apiserver/datastore/kubeapi/kubeapi_suite_test.go create mode 100644 pkg/apiserver/datastore/kubeapi/kubeapi_test.go create mode 100644 pkg/apiserver/datastore/mongodb/mongodb_suite_test.go create mode 100644 pkg/apiserver/datastore/mongodb/mongodb_test.go create mode 100644 pkg/apiserver/model/application.go create mode 100644 pkg/apiserver/model/model.go create mode 100644 pkg/apiserver/rest/usecase/application.go rename pkg/apiserver/rest/usecase/{cluster_usecase.go => cluster.go} (100%) create mode 100644 pkg/apiserver/rest/utils/bcode/application.go create mode 100644 pkg/apiserver/rest/webservice/policy_definition.go create mode 100644 pkg/apiserver/rest/webservice/validate.go create mode 100644 pkg/apiserver/rest/webservice/validate_test.go create mode 100644 pkg/apiserver/rest/webservice/webservice_suite_test.go create mode 100644 pkg/apiserver/rest/webservice/workflow.go create mode 100644 test/e2e-apiserver-test/application_test.go create mode 100644 test/e2e-apiserver-test/suite_test.go diff --git a/.github/workflows/apiserver-test.yaml b/.github/workflows/apiserver-test.yaml new file mode 100644 index 000000000..1f0c93041 --- /dev/null +++ b/.github/workflows/apiserver-test.yaml @@ -0,0 +1,93 @@ +name: apiServer-test + +on: + push: + branches: + - master + - release-* + workflow_dispatch: {} + pull_request: + branches: + - master + - release-* + +env: + # Common versions + GO_VERSION: '1.16' + GOLANGCI_VERSION: 'v1.38' + KIND_VERSION: 'v0.7.0' + +jobs: + + detect-noop: + runs-on: ubuntu-20.04 + outputs: + noop: ${{ steps.noop.outputs.should_skip }} + steps: + - name: Detect No-op Changes + id: noop + uses: fkirc/skip-duplicate-actions@v3.3.0 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + paths_ignore: '["**.md", "**.mdx", "**.png", "**.jpg"]' + do_not_skip: '["workflow_dispatch", "schedule", "push"]' + concurrent_skipping: false + + apiserver-unit-tests: + runs-on: ubuntu-20.04 + needs: detect-noop + if: needs.detect-noop.outputs.noop != 'true' + + steps: + - name: Set up Go + uses: actions/setup-go@v1 + with: + go-version: ${{ env.GO_VERSION }} + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + with: + submodules: true + + - name: Cache Go Dependencies + uses: actions/cache@v2 + with: + path: .work/pkg + key: ${{ runner.os }}-pkg-${{ hashFiles('**/go.sum') }} + restore-keys: ${{ runner.os }}-pkg- + + - name: Install ginkgo + run: | + sudo apt-get install -y golang-ginkgo-dev + + - name: Setup Kind Cluster + uses: engineerd/setup-kind@v0.5.0 + with: + version: ${{ env.KIND_VERSION }} + + - name: install Kubebuilder + uses: RyanSiu1995/kubebuilder-action@v1.2 + with: + version: 3.1.0 + kubebuilderOnly: false + kubernetesVersion: v1.21.2 + + - name: Start MongoDB + uses: supercharge/mongodb-github-action@1.6.0 + with: + mongodb-version: 4.4 + + - name: Run apiserver unit test + run: make unit-test-apiserver + + - name: Upload coverage report + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.txt + flags: apiserver-unittests + name: codecov-umbrella + + - name: Run apiserver e2e test + run: make e2e-apiserver-test diff --git a/.github/workflows/unit-test.yml b/.github/workflows/unit-test.yml index 66ac4abc1..0cf1e1544 100644 --- a/.github/workflows/unit-test.yml +++ b/.github/workflows/unit-test.yml @@ -72,7 +72,7 @@ jobs: version: 3.1.0 kubebuilderOnly: false kubernetesVersion: v1.21.2 - + - name: Run Make test run: make test @@ -81,5 +81,5 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} file: ./coverage.txt - flags: unittests + flags: core-unittests name: codecov-umbrella diff --git a/Makefile b/Makefile index 599c67cd2..ed2641e44 100644 --- a/Makefile +++ b/Makefile @@ -43,11 +43,14 @@ VELA_RUNTIME_ROLLOUT_IMAGE ?= vela-runtime-rollout:latest all: build # Run tests -test: vet lint staticcheck - go test -coverprofile=coverage.txt ./pkg/... ./cmd/... - go test ./references/appfile/... ./references/cli/... ./references/common/... ./references/plugins/... +test: vet lint staticcheck unit-test-core @$(OK) unit-tests pass +unit-test-core: + go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... ./references/... | grep -v apiserver) +unit-test-apiserver: + go test -coverprofile=coverage.txt $(shell go list ./pkg/... ./cmd/... | grep apiserver) + # Build vela cli binary build: fmt vet lint staticcheck vela-cli kubectl-vela @$(OK) build succeed @@ -163,6 +166,10 @@ e2e-api-test: ginkgo -v -skipPackage capability,setup,application -r e2e ginkgo -v -r e2e/application +e2e-apiserver-test: + ginkgo -v ./test/e2e-apiserver-test + @$(OK) tests pass + e2e-test: # Run e2e test ginkgo -v --skip="rollout related e2e-test." ./test/e2e-test @@ -178,7 +185,7 @@ e2e-multicluster-test: compatibility-test: vet lint staticcheck generate-compatibility-testdata # Run compatibility test with old crd - COMPATIBILITY_TEST=TRUE go test -race ./pkg/... + COMPATIBILITY_TEST=TRUE go test -race $(shell go list ./pkg/... | grep -v apiserver) @$(OK) compatibility-test pass generate-compatibility-testdata: diff --git a/cmd/apiserver/main.go b/cmd/apiserver/main.go index 82b0374c6..d702c0c2c 100644 --- a/cmd/apiserver/main.go +++ b/cmd/apiserver/main.go @@ -30,7 +30,7 @@ import ( ) func main() { - s := &server{} + s := &Server{} flag.StringVar(&s.restCfg.BindAddr, "bind-addr", "0.0.0.0:8000", "The bind address used to serve the http APIs.") flag.StringVar(&s.restCfg.MetricPath, "metrics-path", "/metrics", "The path to expose the metrics.") flag.StringVar(&s.restCfg.Datastore.Type, "datastore-type", "kubeapi", "Metadata storage driver type, support kubeapi and mongodb") @@ -57,11 +57,12 @@ func main() { log.Logger.Infof("See you next time!") } -type server struct { +// Server apiserver +type Server struct { restCfg rest.Config } -func (s *server) run() error { +func (s *Server) run() error { log.Logger.Infof("KubeVela information: version: %v, gitRevision: %v", version.VelaVersion, version.GitRevision) ctx := context.Background() diff --git a/contribute/developer-guide.md b/contribute/developer-guide.md index 6af8b6f9e..51fa92a64 100644 --- a/contribute/developer-guide.md +++ b/contribute/developer-guide.md @@ -115,6 +115,12 @@ You can try use your local built binaries follow [the documentation](https://kub make test ``` +To execute the unit test of the API module, the mongodb service needs to exist locally. + +```shell script +make unit-test-apiserver +``` + ### E2E test **Before e2e test start, make sure you have vela-core running.** @@ -125,10 +131,16 @@ make core-run Start to test. -``` +```shell script make e2e-test ``` +To execute the e2e test of the API module, the mongodb service needs to exist locally. + +```shell script +make e2e-apiserver-test +``` + ## Contribute Docs Please read [the documentation](https://github.com/oam-dev/kubevela/tree/master/docs/README.md) before contributing to the docs. diff --git a/docs/apidoc/swagger.json b/docs/apidoc/swagger.json new file mode 100644 index 000000000..38dbcaf31 --- /dev/null +++ b/docs/apidoc/swagger.json @@ -0,0 +1,2695 @@ +{ + "swagger": "2.0", + "info": { + "description": "Kubevela api doc", + "title": "Kubevela api doc", + "contact": { + "name": "kubevela", + "url": "https://kubevela.io/", + "email": "feedback@mail.kubevela.io" + }, + "license": { + "name": "Apache License 2.0", + "url": "https://github.com/oam-dev/kubevela/blob/master/LICENSE" + }, + "version": "v1beta1" + }, + "paths": { + "/api/v1/applications": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "list all applications", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + }, + { + "type": "string", + "description": "Namespace-based search", + "name": "namespace", + "in": "query" + }, + { + "type": "string", + "description": "Cluster-based search", + "name": "cluster", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create one application", + "operationId": "noop", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail one application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "delete one application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}/components": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "gets the component topology of the application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "list components that deployed in define cluster", + "name": "cluster", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create component for application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateComponentRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}/deploy": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "deploy or update the application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}/policies": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create policy for application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreatePolicyRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}/policies/{policyName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail policy for application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application policy", + "name": "policyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "detail policy for application", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the application policy", + "name": "policyName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/applications/{name}/template": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "application" + ], + "summary": "create one application template", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the application", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateApplicationTemplateRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/clusters": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list all clusters", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create cluster", + "operationId": "createKubeCluster", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/*v1.CreateClusterRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/clusters/{clusterName}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "detail cluster info", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the cluster", + "name": "clusterName", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/componentdefinitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "componentdefinition" + ], + "summary": "list all componentdefinition", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "if specified, query the componentdefinition supported by the cluster where the application resides.", + "name": "appName", + "in": "query" + }, + { + "type": "string", + "description": "if specified, query the componentdefinition supported by the cluster.", + "name": "clusterName", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/namespaces": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "list all namespaces", + "operationId": "noop", + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "create namespace", + "operationId": "noop", + "parameters": [ + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.CreateNamespaceRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/namespaces/{namespace}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "get one namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/namespaces/{namespace}/applications/:appname": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "get the specified oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.ApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "namespace" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/policydefinitions": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "policydefinition" + ], + "summary": "list all policydefinition", + "operationId": "noop", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/api/v1/workflows/{name}": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "detail application workflow", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow, Currently, the application name is used.", + "name": "name", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + }, + "put": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "create or update application workflow config", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.UpdateWorkflowRequest" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/api/v1/workflows/{name}/records": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "query application workflow execution record", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the workflow", + "name": "name", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Query the page number.", + "name": "page", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Query the page size number.", + "name": "pageSize", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/v1/catalogs": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "cluster" + ], + "summary": "list all clusters", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "Fuzzy search based on name or description", + "name": "query", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/map[string]string" + } + }, + "500": { + "description": "Bummer, something went wrong" + } + } + } + }, + "/v1/{namespace}/applications/:appname": { + "get": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam" + ], + "summary": "get the specified oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "delete": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/v1/{namespace}/applications/{appname}": { + "post": { + "consumes": [ + "application/xml", + "application/json" + ], + "produces": [ + "application/json", + "application/xml" + ], + "tags": [ + "oam" + ], + "summary": "create or update oam application in the specified namespace", + "operationId": "noop", + "parameters": [ + { + "type": "string", + "description": "identifier of the namespace", + "name": "namespace", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "identifier of the oam application", + "name": "appname", + "in": "path", + "required": true + }, + { + "name": "body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/v1.ApplicationRequest" + } + } + ], + "responses": { + "200": { + "description": "OK" + } + } + } + } + }, + "definitions": { + "common.AppRolloutStatus": { + "required": [ + "rollingState", + "batchRollingState", + "currentBatch", + "upgradedReplicas", + "upgradedReadyReplicas", + "lastTargetAppRevision" + ], + "properties": { + "LastSourceAppRevision": { + "type": "string" + }, + "batchRollingState": { + "type": "string" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "currentBatch": { + "type": "integer", + "format": "int32" + }, + "lastAppliedPodTemplateIdentifier": { + "type": "string" + }, + "lastTargetAppRevision": { + "type": "string" + }, + "rollingState": { + "type": "string" + }, + "rolloutOriginalSize": { + "type": "integer", + "format": "int32" + }, + "rolloutTargetSize": { + "type": "integer", + "format": "int32" + }, + "targetGeneration": { + "type": "string" + }, + "upgradedReadyReplicas": { + "type": "integer", + "format": "int32" + }, + "upgradedReplicas": { + "type": "integer", + "format": "int32" + } + } + }, + "common.AppStatus": { + "properties": { + "appliedResources": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ClusterObjectReference" + } + }, + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ObjectReference" + } + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "latestRevision": { + "$ref": "#/definitions/common.Revision" + }, + "observedGeneration": { + "type": "integer", + "format": "int64" + }, + "resourceTracker": { + "$ref": "#/definitions/v1.ObjectReference" + }, + "rollout": { + "$ref": "#/definitions/common.AppRolloutStatus" + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponentStatus" + } + }, + "status": { + "type": "string" + }, + "workflow": { + "$ref": "#/definitions/common.WorkflowStatus" + } + } + }, + "common.ApplicationComponent": { + "required": [ + "name", + "type" + ], + "properties": { + "externalRevision": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "scopes": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationTrait" + } + }, + "type": { + "type": "string" + } + } + }, + "common.ApplicationComponentStatus": { + "required": [ + "name", + "healthy" + ], + "properties": { + "env": { + "type": "string" + }, + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ObjectReference" + } + }, + "traits": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationTraitStatus" + } + }, + "workloadDefinition": { + "$ref": "#/definitions/common.WorkloadGVK" + } + } + }, + "common.ApplicationTrait": { + "required": [ + "type" + ], + "properties": { + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.ApplicationTraitStatus": { + "required": [ + "type", + "healthy" + ], + "properties": { + "healthy": { + "type": "boolean" + }, + "message": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.ClusterObjectReference": { + "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "cluster": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "fieldPath": { + "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", + "type": "string" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "type": "string" + }, + "resourceVersion": { + "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", + "type": "string" + } + } + }, + "common.Revision": { + "required": [ + "name", + "revision" + ], + "properties": { + "name": { + "type": "string" + }, + "revision": { + "type": "integer", + "format": "int64" + }, + "revisionHash": { + "type": "string" + } + } + }, + "common.SubStepsStatus": { + "properties": { + "mode": { + "type": "string" + }, + "stepIndex": { + "type": "integer", + "format": "int32" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowSubStepStatus" + } + } + } + }, + "common.WorkflowStatus": { + "required": [ + "mode", + "suspend", + "terminated" + ], + "properties": { + "appRevision": { + "type": "string" + }, + "contextBackend": { + "$ref": "#/definitions/v1.ObjectReference" + }, + "mode": { + "type": "string" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/common.WorkflowStepStatus" + } + }, + "suspend": { + "type": "boolean" + }, + "terminated": { + "type": "boolean" + } + } + }, + "common.WorkflowStepStatus": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "subSteps": { + "$ref": "#/definitions/common.SubStepsStatus" + }, + "type": { + "type": "string" + } + } + }, + "common.WorkflowSubStepStatus": { + "required": [ + "id" + ], + "properties": { + "id": { + "type": "string" + }, + "message": { + "type": "string" + }, + "name": { + "type": "string" + }, + "phase": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "common.WorkloadGVK": { + "required": [ + "apiVersion", + "kind" + ], + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + } + } + }, + "common.inputItem": { + "required": [ + "parameterKey", + "from" + ], + "properties": { + "from": { + "type": "string" + }, + "parameterKey": { + "type": "string" + } + } + }, + "common.outputItem": { + "required": [ + "valueFrom", + "name" + ], + "properties": { + "name": { + "type": "string" + }, + "valueFrom": { + "type": "string" + } + } + }, + "condition.Condition": { + "required": [ + "type", + "status", + "lastTransitionTime", + "reason" + ], + "properties": { + "lastTransitionTime": { + "type": "string" + }, + "message": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "condition.ConditionedStatus": { + "properties": { + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + } + } + }, + "map[string]string": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "types.Parameter": { + "required": [ + "name" + ], + "properties": { + "alias": { + "type": "string" + }, + "default": { + "$ref": "#/definitions/types.Parameter.default" + }, + "ignore": { + "type": "boolean" + }, + "jsonType": { + "type": "string" + }, + "name": { + "type": "string" + }, + "required": { + "type": "boolean" + }, + "short": { + "type": "string" + }, + "type": { + "type": "integer", + "format": "int32" + }, + "usage": { + "type": "string" + } + } + }, + "types.Parameter.default": {}, + "v1.ApplicationBase": { + "required": [ + "name", + "namespace", + "description", + "createTime", + "updateTime", + "icon", + "status", + "gatewayRule" + ], + "properties": { + "clusterList": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "gatewayRule": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.GatewayRule" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + } + } + }, + "v1.ApplicationRequest": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponent" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.AppPolicy" + } + }, + "workflow": { + "$ref": "#/definitions/v1beta1.Workflow" + } + } + }, + "v1.ApplicationResourceInfo": { + "required": [ + "componentNum" + ], + "properties": { + "componentNum": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ApplicationResponse": { + "required": [ + "apiVersion", + "kind", + "spec", + "status" + ], + "properties": { + "apiVersion": { + "type": "string" + }, + "kind": { + "type": "string" + }, + "spec": { + "$ref": "#/definitions/v1beta1.ApplicationSpec" + }, + "status": { + "$ref": "#/definitions/common.AppStatus" + } + } + }, + "v1.ApplicationTemplateBase": { + "required": [ + "templateName" + ], + "properties": { + "templateName": { + "type": "string" + }, + "versions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationTemplateVersion" + } + } + } + }, + "v1.ApplicationTemplateVersion": { + "required": [ + "version", + "description", + "createUser", + "createTime", + "updateTime" + ], + "properties": { + "createTime": { + "type": "string", + "format": "date-time" + }, + "createUser": { + "type": "string" + }, + "description": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "version": { + "type": "string" + } + } + }, + "v1.ClusterBase": { + "required": [ + "name", + "description", + "icon", + "labels", + "status", + "reason" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, + "v1.ClusterResourceInfo": { + "required": [ + "workerNumber", + "masterNumber", + "memoryCapacity", + "cpuCapacity" + ], + "properties": { + "cpuCapacity": { + "type": "integer", + "format": "int64" + }, + "gpuCapacity": { + "type": "integer", + "format": "int64" + }, + "masterNumber": { + "type": "integer", + "format": "int32" + }, + "memoryCapacity": { + "type": "integer", + "format": "int64" + }, + "storageClassList": { + "type": "array", + "items": { + "type": "string" + } + }, + "workerNumber": { + "type": "integer", + "format": "int32" + } + } + }, + "v1.ComponentBase": { + "required": [ + "name", + "description", + "componentType", + "bindClusters", + "dependsOn", + "deployVersion" + ], + "properties": { + "bindClusters": { + "type": "array", + "items": { + "type": "string" + } + }, + "componentType": { + "type": "string" + }, + "creator": { + "type": "string" + }, + "dependsOn": { + "type": "array", + "items": { + "type": "string" + } + }, + "deployVersion": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.ComponentDefinitionBase": { + "required": [ + "name", + "description", + "icon", + "requiredParams" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "name": { + "type": "string" + }, + "requiredParams": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Parameter" + } + } + } + }, + "v1.ComponentListResponse": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ComponentBase" + } + } + } + }, + "v1.CreateApplicationRequest": { + "required": [ + "name", + "namespace", + "description", + "icon" + ], + "properties": { + "clusterList": { + "type": "array", + "items": { + "type": "string" + } + }, + "deploy": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "yamlConfig": { + "type": "string" + } + } + }, + "v1.CreateApplicationTemplateRequest": { + "required": [ + "templateName", + "version", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "templateName": { + "type": "string" + }, + "version": { + "type": "string" + } + } + }, + "v1.CreateClusterRequest": { + "required": [ + "name", + "icon", + "kubeConfig" + ], + "properties": { + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "kubeConfig": { + "type": "string" + }, + "kubeConfigSecret": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + } + } + }, + "v1.CreateComponentRequest": { + "required": [ + "appName", + "name", + "description", + "componentType", + "bindClusters" + ], + "properties": { + "appName": { + "type": "string" + }, + "bindClusters": { + "type": "array", + "items": { + "type": "string" + } + }, + "componentType": { + "type": "string" + }, + "description": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "properties": { + "type": "string" + } + } + }, + "v1.CreateNamespaceRequest": { + "required": [ + "name", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.CreatePolicyRequest": { + "required": [ + "name", + "type", + "properties" + ], + "properties": { + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.DetailApplicationResponse": { + "required": [ + "updateTime", + "icon", + "status", + "gatewayRule", + "name", + "namespace", + "description", + "createTime", + "policies", + "status", + "resourceInfo", + "workflowStatus" + ], + "properties": { + "clusterList": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterBase" + } + }, + "createTime": { + "type": "string", + "format": "date-time" + }, + "description": { + "type": "string" + }, + "gatewayRule": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.GatewayRule" + } + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "policies": { + "type": "array", + "items": { + "type": "string" + } + }, + "resourceInfo": { + "$ref": "#/definitions/v1.ApplicationResourceInfo" + }, + "status": { + "type": "string" + }, + "updateTime": { + "type": "string", + "format": "date-time" + }, + "workflowStatus": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStepStatus" + } + } + } + }, + "v1.DetailClusterResponse": { + "required": [ + "name", + "description", + "icon", + "labels", + "status", + "reason", + "resourceInfo" + ], + "properties": { + "dashboardURL": { + "type": "string" + }, + "description": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "name": { + "type": "string" + }, + "reason": { + "type": "string" + }, + "remoteManageURL": { + "type": "string" + }, + "resourceInfo": { + "$ref": "#/definitions/v1.ClusterResourceInfo" + }, + "status": { + "type": "string" + } + } + }, + "v1.DetailPolicyResponse": { + "required": [ + "name", + "type", + "properties" + ], + "properties": { + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.DetailWorkflowResponse": { + "required": [ + "enable", + "workflowRecord" + ], + "properties": { + "enable": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + }, + "workflowRecord": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + } + }, + "v1.GatewayRule": { + "required": [ + "ruleType", + "address", + "protocol", + "componentName", + "componentPort" + ], + "properties": { + "address": { + "type": "string" + }, + "componentName": { + "type": "string" + }, + "componentPort": { + "type": "integer", + "format": "int32" + }, + "protocol": { + "type": "string" + }, + "ruleType": { + "type": "string" + } + } + }, + "v1.ListApplicationResponse": { + "required": [ + "applications" + ], + "properties": { + "applications": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ApplicationBase" + } + } + } + }, + "v1.ListClusterResponse": { + "required": [ + "clusters" + ], + "properties": { + "clusters": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ClusterBase" + } + } + } + }, + "v1.ListComponentDefinitionResponse": { + "required": [ + "componentDefinitions" + ], + "properties": { + "componentDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.ComponentDefinitionBase" + } + } + } + }, + "v1.ListNamespaceResponse": { + "required": [ + "namesapces" + ], + "properties": { + "namesapces": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.NamesapceBase" + } + } + } + }, + "v1.ListPolicyDefinitionResponse": { + "required": [ + "policyDefinitions" + ], + "properties": { + "policyDefinitions": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.PolicyDefinition" + } + } + } + }, + "v1.ListWorkflowRecordsResponse": { + "required": [ + "records", + "total" + ], + "properties": { + "records": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowRecord" + } + }, + "total": { + "type": "integer", + "format": "int64" + } + } + }, + "v1.NamesapceBase": { + "required": [ + "name", + "description" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.NamesapceDetailResponse": { + "required": [ + "name", + "description", + "clusterBind" + ], + "properties": { + "clusterBind": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "description": { + "type": "string" + }, + "name": { + "type": "string" + } + } + }, + "v1.ObjectReference": { + "description": "ObjectReference contains enough information to let you inspect or modify the referred object.", + "properties": { + "apiVersion": { + "description": "API version of the referent.", + "type": "string" + }, + "fieldPath": { + "description": "If referring to a piece of an object instead of an entire object, this string should contain a valid JSON/Go field access statement, such as desiredState.manifest.containers[2]. For example, if the object reference is to a container within a pod, this would take on a value like: \"spec.containers{name}\" (where \"name\" refers to the name of the container that triggered the event) or if no container name is specified \"spec.containers[2]\" (container with index 2 in this pod). This syntax is chosen only to have some well-defined way of referencing a part of an object.", + "type": "string" + }, + "kind": { + "description": "Kind of the referent. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds", + "type": "string" + }, + "name": { + "description": "Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names", + "type": "string" + }, + "namespace": { + "description": "Namespace of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/", + "type": "string" + }, + "resourceVersion": { + "description": "Specific resourceVersion to which this reference is made, if any. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#concurrency-control-and-consistency", + "type": "string" + }, + "uid": { + "description": "UID of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#uids", + "type": "string" + } + } + }, + "v1.PolicyDefinition": { + "required": [ + "name", + "description", + "parameters" + ], + "properties": { + "description": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parameters": { + "type": "array", + "items": { + "$ref": "#/definitions/types.Parameter" + } + } + } + }, + "v1.UpdateWorkflowRequest": { + "required": [ + "enable" + ], + "properties": { + "enable": { + "type": "boolean" + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1.WorkflowStep" + } + } + } + }, + "v1.WorkflowRecord": {}, + "v1.WorkflowStep": { + "required": [ + "name", + "type" + ], + "properties": { + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1.WorkflowStepStatus": { + "required": [ + "name", + "status", + "takeTime" + ], + "properties": { + "name": { + "type": "string" + }, + "status": { + "type": "string" + }, + "takeTime": { + "type": "integer", + "format": "integer" + } + } + }, + "v1alpha1.CanaryMetric": { + "required": [ + "name" + ], + "properties": { + "interval": { + "type": "string" + }, + "metricsRange": { + "$ref": "#/definitions/v1alpha1.MetricsExpectedRange" + }, + "name": { + "type": "string" + }, + "templateRef": { + "$ref": "#/definitions/v1.ObjectReference" + } + } + }, + "v1alpha1.MetricsExpectedRange": { + "properties": { + "max": { + "type": "string" + }, + "min": { + "type": "string" + } + } + }, + "v1alpha1.RolloutBatch": { + "properties": { + "batchRolloutWebhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook" + } + }, + "canaryMetric": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.CanaryMetric" + } + }, + "instanceInterval": { + "type": "integer", + "format": "int32" + }, + "maxUnavailable": { + "type": "string" + }, + "podList": { + "type": "array", + "items": { + "type": "string" + } + }, + "replicas": { + "type": "string" + } + } + }, + "v1alpha1.RolloutPlan": { + "properties": { + "batchPartition": { + "type": "integer", + "format": "int32" + }, + "canaryMetric": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.CanaryMetric" + } + }, + "numBatches": { + "type": "integer", + "format": "int32" + }, + "paused": { + "type": "boolean" + }, + "rolloutBatches": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutBatch" + } + }, + "rolloutStrategy": { + "type": "string" + }, + "rolloutWebhooks": { + "type": "array", + "items": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook" + } + }, + "targetSize": { + "type": "integer", + "format": "int32" + } + } + }, + "v1alpha1.RolloutStatus": { + "required": [ + "rollingState", + "batchRollingState", + "currentBatch", + "upgradedReplicas", + "upgradedReadyReplicas" + ], + "properties": { + "batchRollingState": { + "type": "string" + }, + "conditions": { + "type": "array", + "items": { + "$ref": "#/definitions/condition.Condition" + } + }, + "currentBatch": { + "type": "integer", + "format": "int32" + }, + "lastAppliedPodTemplateIdentifier": { + "type": "string" + }, + "rollingState": { + "type": "string" + }, + "rolloutOriginalSize": { + "type": "integer", + "format": "int32" + }, + "rolloutTargetSize": { + "type": "integer", + "format": "int32" + }, + "targetGeneration": { + "type": "string" + }, + "upgradedReadyReplicas": { + "type": "integer", + "format": "int32" + }, + "upgradedReplicas": { + "type": "integer", + "format": "int32" + } + } + }, + "v1alpha1.RolloutWebhook": { + "required": [ + "type", + "name", + "url" + ], + "properties": { + "expectedStatus": { + "type": "array", + "items": { + "type": "integer" + } + }, + "metadata": { + "$ref": "#/definitions/v1alpha1.RolloutWebhook.metadata" + }, + "method": { + "type": "string" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "url": { + "type": "string" + } + } + }, + "v1alpha1.RolloutWebhook.metadata": { + "type": "object", + "additionalProperties": { + "type": "string" + } + }, + "v1beta1.AppPolicy": { + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string" + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + }, + "v1beta1.ApplicationSpec": { + "required": [ + "components" + ], + "properties": { + "components": { + "type": "array", + "items": { + "$ref": "#/definitions/common.ApplicationComponent" + } + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.AppPolicy" + } + }, + "rolloutPlan": { + "$ref": "#/definitions/v1alpha1.RolloutPlan" + }, + "workflow": { + "$ref": "#/definitions/v1beta1.Workflow" + } + } + }, + "v1beta1.Workflow": { + "properties": { + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/v1beta1.WorkflowStep" + } + } + } + }, + "v1beta1.WorkflowStep": { + "required": [ + "name", + "type" + ], + "properties": { + "inputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.inputItem" + } + }, + "name": { + "type": "string" + }, + "outputs": { + "type": "array", + "items": { + "$ref": "#/definitions/common.outputItem" + } + }, + "properties": { + "type": "string" + }, + "type": { + "type": "string" + } + } + } + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 35a6d8834..6e69fb99d 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/emicklei/go-restful/v3 v3.0.0-rc2 github.com/evanphx/json-patch v4.11.0+incompatible github.com/fatih/color v1.12.0 + github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/gertd/go-pluralize v0.1.7 github.com/getkin/kin-openapi v0.34.0 github.com/go-logr/logr v0.4.0 @@ -51,6 +52,8 @@ require ( go.mongodb.org/mongo-driver v1.5.1 go.uber.org/zap v1.18.1 golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602 + golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 // indirect + golang.org/x/tools v0.1.6 // indirect gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b gotest.tools v2.2.0+incompatible helm.sh/helm/v3 v3.6.1 diff --git a/go.sum b/go.sum index 09a32681b..18fedb114 100644 --- a/go.sum +++ b/go.sum @@ -462,8 +462,9 @@ github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVB github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fullstorydev/grpcurl v1.6.0/go.mod h1:ZQ+ayqbKMJNhzLmbpCiurTVlaK2M/3nqZCxaQ2Ze/sM= github.com/fvbommel/sortorder v1.0.1/go.mod h1:uk88iVf1ovNn1iLfgUVU2F9o5eO30ui720w+kxuqRs0= github.com/fzipp/gocyclo v0.3.1/go.mod h1:DJHO6AUmbdqj2ET4Z9iArSuwWgYDRryYt2wASxc7x3E= @@ -1592,6 +1593,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43 h1:+lm10QQTNSBd8DVTNGHx7o/IKu9HYDvLMffDhbyLccI= github.com/yvasiyarov/go-metrics v0.0.0-20140926110328-57bccd1ccd43/go.mod h1:aX5oPXxHm3bOH+xeAttToC8pqch2ScQN/JoXYupl6xs= github.com/yvasiyarov/gorelic v0.0.0-20141212073537-a9bba5b9ab50 h1:hlE8//ciYMztlGpl/VA+Zm1AcTPHYkHJPbHqE6WJUXE= @@ -1842,8 +1844,9 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210520170846-37e1c6afe023 h1:ADo5wSpq2gqaCGQWzk7S5vd//0iyyLeAratkEoG5dLE= golang.org/x/net v0.0.0-20210520170846-37e1c6afe023/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1978,8 +1981,10 @@ golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069 h1:siQdpVirKtzPhKl3lZWozZraCFObP8S1v6PRp0bLrtU= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6 h1:foEbQz/B0Oz6YIqu/69kfXPYeFQAuuMYFkjaqXzl5Wo= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -2126,8 +2131,9 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5 h1:ouewzE6p+/VEB31YYnTbEJdi8pFqKp4P4n85vwo3DHA= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.6 h1:SIasE1FVIQOWz2GEAHFOmoW7xchJcqlucjSULTL0Ag4= +golang.org/x/tools v0.1.6/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/pkg/apiserver/clients/kubeclient.go b/pkg/apiserver/clients/kubeclient.go new file mode 100644 index 000000000..8e3733022 --- /dev/null +++ b/pkg/apiserver/clients/kubeclient.go @@ -0,0 +1,47 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package clients + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/config" + + "github.com/oam-dev/kubevela/pkg/utils/common" +) + +var kubeClient client.Client + +// SetKubeClient for test +func SetKubeClient(c client.Client) { + kubeClient = c +} + +// GetKubeClient create and return kube runtime client +func GetKubeClient() (client.Client, error) { + if kubeClient != nil { + return kubeClient, nil + } + conf, err := config.GetConfig() + if err != nil { + return nil, err + } + k8sClient, err := client.New(conf, client.Options{Scheme: common.Scheme}) + if err != nil { + return nil, err + } + return k8sClient, nil +} diff --git a/pkg/apiserver/datastore/datastore.go b/pkg/apiserver/datastore/datastore.go index 312da9be2..34d70f112 100644 --- a/pkg/apiserver/datastore/datastore.go +++ b/pkg/apiserver/datastore/datastore.go @@ -18,8 +18,41 @@ package datastore import ( "context" + "fmt" + "reflect" ) +var ( + // ErrPrimaryEmpty Error that primary key is empty. + ErrPrimaryEmpty = NewDBError(fmt.Errorf("entity primary is empty")) + + // ErrTableNameEmpty Error that table name is empty. + ErrTableNameEmpty = NewDBError(fmt.Errorf("entity table name is empty")) + + // ErrNilEntity Error that entity is nil + ErrNilEntity = NewDBError(fmt.Errorf("entity is nil")) + + // ErrRecordExist Error that entity primary key is exist + ErrRecordExist = NewDBError(fmt.Errorf("data record is exist")) + + // ErrRecordNotExist Error that entity primary key is not exist + ErrRecordNotExist = NewDBError(fmt.Errorf("data record is not exist")) +) + +// DBError datastore error +type DBError struct { + err error +} + +func (d *DBError) Error() string { + return d.err.Error() +} + +// NewDBError new datastore error +func NewDBError(err error) error { + return &DBError{err: err} +} + // Config datastore config type Config struct { Type string @@ -27,31 +60,48 @@ type Config struct { Database string } +// Entity database data model +type Entity interface { + PrimaryKey() string + TableName() string +} + +// NewEntity Create a new object based on the input type +func NewEntity(in Entity) (Entity, error) { + if in == nil { + return nil, ErrNilEntity + } + t := reflect.TypeOf(in) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + new := reflect.New(t) + return new.Interface().(Entity), nil +} + +// ListOptions list api options +type ListOptions struct { + Page int + PageSize int +} + // DataStore datastore interface type DataStore interface { - Add(ctx context.Context, kind string, entity interface{}) error + // add entity to database, Name() and TableName() can't return zero value. + Add(ctx context.Context, entity Entity) error - Put(ctx context.Context, kind, name string, entity interface{}) error + // Update entity to database, Name() and TableName() can't return zero value. + Put(ctx context.Context, entity Entity) error - Delete(ctx context.Context, kind, name string) error + // Delete entity from database, Name() and TableName() can't return zero value. + Delete(ctx context.Context, entity Entity) error - Get(ctx context.Context, kind, name string, decodeTo interface{}) error + // Get entity from database, Name() and TableName() can't return zero value. + Get(ctx context.Context, entity Entity) error - // Find executes a find command and returns an iterator over the matching items. - Find(ctx context.Context, kind string) (Iterator, error) + // TableName() can't return zero value. + List(ctx context.Context, query Entity, options *ListOptions) ([]Entity, error) - FindOne(ctx context.Context, kind, name string) (Iterator, error) - - IsExist(ctx context.Context, kind, name string) (bool, error) -} - -// Iterator dataset query -type Iterator interface { - // Next gets the next item for this cursor. - Next(ctx context.Context) bool - - // Decode will unmarshal the current item into given entity. - Decode(entity interface{}) error - - Close(ctx context.Context) error + // IsExist Name() and TableName() can't return zero value. + IsExist(ctx context.Context, entity Entity) (bool, error) } diff --git a/pkg/apiserver/datastore/datastore_suite_test.go b/pkg/apiserver/datastore/datastore_suite_test.go new file mode 100644 index 000000000..a26943174 --- /dev/null +++ b/pkg/apiserver/datastore/datastore_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datastore + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestDatastore(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Datastore Suite") +} diff --git a/pkg/apiserver/datastore/datastore_test.go b/pkg/apiserver/datastore/datastore_test.go new file mode 100644 index 000000000..26feeff76 --- /dev/null +++ b/pkg/apiserver/datastore/datastore_test.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package datastore + +import ( + "encoding/json" + "fmt" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/model" +) + +var _ = Describe("Test new entity function", func() { + + It("Test new application entity", func() { + var app model.Application + new, err := NewEntity(&app) + Expect(err).To(BeNil()) + json.Unmarshal([]byte(`{"name":"demo"}`), new) + Expect(err).To(BeNil()) + diff := cmp.Diff(new.PrimaryKey(), "demo") + Expect(diff).Should(BeEmpty()) + }) + + It("Test new multiple application entity", func() { + var app model.Application + var list []Entity + var n = 3 + for n > 0 { + new, err := NewEntity(&app) + Expect(err).To(BeNil()) + json.Unmarshal([]byte(fmt.Sprintf(`{"name":"demo %d"}`, n)), new) + Expect(err).To(BeNil()) + diff := cmp.Diff(new.PrimaryKey(), fmt.Sprintf("demo %d", n)) + Expect(diff).Should(BeEmpty()) + list = append(list, new) + n-- + } + diff := cmp.Diff(list[0].PrimaryKey(), "demo 3") + Expect(diff).Should(BeEmpty()) + }) + +}) diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi.go b/pkg/apiserver/datastore/kubeapi/kubeapi.go index b9b271a05..0941c6d78 100644 --- a/pkg/apiserver/datastore/kubeapi/kubeapi.go +++ b/pkg/apiserver/datastore/kubeapi/kubeapi.go @@ -18,50 +18,224 @@ package kubeapi import ( "context" + "encoding/json" + "fmt" + "strings" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" ) type kubeapi struct { - // kubeclient client.Client + kubeclient client.Client + namespace string } // New new kubeapi datastore instance +// Data is stored using ConfigMap. func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) { - return &kubeapi{}, nil + kubeClient, err := clients.GetKubeClient() + if err != nil { + return nil, err + } + if cfg.Database == "" { + cfg.Database = "kubevela_store" + } + var namespace corev1.Namespace + if err := kubeClient.Get(ctx, types.NamespacedName{Name: cfg.Database}, &namespace); apierrors.IsNotFound(err) { + if err := kubeClient.Create(ctx, &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: cfg.Database, + Annotations: map[string]string{"description": "For kubevela apiserver metadata storage."}, + }}); err != nil { + return nil, fmt.Errorf("create namesapce failure %w", err) + } + } + return &kubeapi{ + kubeclient: kubeClient, + namespace: cfg.Database, + }, nil +} + +func generateName(entity datastore.Entity) string { + name := fmt.Sprintf("veladatabase-%s-%s", entity.TableName(), entity.PrimaryKey()) + return strings.ReplaceAll(name, "_", "-") +} + +func (m *kubeapi) generateConfigMap(entity datastore.Entity) *corev1.ConfigMap { + data, _ := json.Marshal(entity) + var configMap = corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: generateName(entity), + Namespace: m.namespace, + Labels: map[string]string{ + "table": entity.TableName(), + "primaryKey": entity.PrimaryKey(), + }, + }, + BinaryData: map[string][]byte{ + "data": data, + }, + } + return &configMap } // Add add data model -func (m *kubeapi) Add(ctx context.Context, kind string, entity interface{}) error { +func (m *kubeapi) Add(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + configMap := m.generateConfigMap(entity) + if err := m.kubeclient.Create(ctx, configMap); err != nil { + if apierrors.IsAlreadyExists(err) { + return datastore.ErrRecordExist + } + return datastore.NewDBError(err) + } return nil } // Get get data model -func (m *kubeapi) Get(ctx context.Context, kind, name string, decodeTo interface{}) error { +func (m *kubeapi) Get(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + var configMap corev1.ConfigMap + if err := m.kubeclient.Get(ctx, types.NamespacedName{Namespace: m.namespace, Name: generateName(entity)}, &configMap); err != nil { + if apierrors.IsNotFound(err) { + return datastore.ErrRecordNotExist + } + return datastore.NewDBError(err) + } + if err := json.Unmarshal(configMap.BinaryData["data"], entity); err != nil { + return datastore.NewDBError(err) + } return nil } // Put update data model -func (m *kubeapi) Put(ctx context.Context, kind, name string, entity interface{}) error { +func (m *kubeapi) Put(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + var configMap corev1.ConfigMap + if err := m.kubeclient.Get(ctx, types.NamespacedName{Namespace: m.namespace, Name: generateName(entity)}, &configMap); err != nil { + if apierrors.IsNotFound(err) { + return datastore.ErrRecordNotExist + } + return datastore.NewDBError(err) + } + data, err := json.Marshal(entity) + if err != nil { + return datastore.NewDBError(err) + } + configMap.BinaryData["data"] = data + if err := m.kubeclient.Update(ctx, &configMap); err != nil { + return datastore.NewDBError(err) + } return nil } -// Find find data model -func (m *kubeapi) Find(ctx context.Context, kind string) (datastore.Iterator, error) { - return nil, nil -} - -// FindOne find one data model -func (m *kubeapi) FindOne(ctx context.Context, kind, name string) (datastore.Iterator, error) { - return nil, nil -} - // IsExist determine whether data exists. -func (m *kubeapi) IsExist(ctx context.Context, kind, name string) (bool, error) { +func (m *kubeapi) IsExist(ctx context.Context, entity datastore.Entity) (bool, error) { + if entity.PrimaryKey() == "" { + return false, datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return false, datastore.ErrTableNameEmpty + } + var configMap corev1.ConfigMap + if err := m.kubeclient.Get(ctx, types.NamespacedName{Namespace: m.namespace, Name: generateName(entity)}, &configMap); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + return false, datastore.NewDBError(err) + } return true, nil } // Delete delete data -func (m *kubeapi) Delete(ctx context.Context, kind, name string) error { +func (m *kubeapi) Delete(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + if err := m.kubeclient.Delete(ctx, m.generateConfigMap(entity)); err != nil { + if apierrors.IsNotFound(err) { + return datastore.ErrRecordNotExist + } + return datastore.NewDBError(err) + } return nil } + +// TableName() can't return zero value. +func (m *kubeapi) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) { + if entity.TableName() == "" { + return nil, datastore.ErrTableNameEmpty + } + selector, err := labels.Parse(fmt.Sprintf("table=%s", entity.TableName())) + if err != nil { + return nil, datastore.NewDBError(err) + } + options := &client.ListOptions{ + LabelSelector: selector, + } + var skip, limit int64 + if op != nil && op.PageSize > 0 && op.Page > 0 { + skip = int64(op.PageSize * (op.Page - 1)) + limit = int64(op.PageSize * op.Page) + if skip < 0 { + skip = 0 + } + if limit < 0 { + limit = skip + } + options.Limit = limit + } + var configMaps corev1.ConfigMapList + if err := m.kubeclient.List(ctx, &configMaps, options); err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, datastore.NewDBError(err) + } + items := configMaps.Items + if op != nil && op.PageSize > 0 && op.Page > 0 { + if len(configMaps.Items) > int(limit) { + items = configMaps.Items[skip:limit] + } else { + items = configMaps.Items[skip:] + } + } + var list []datastore.Entity + for _, item := range items { + ent, err := datastore.NewEntity(entity) + if err != nil { + return nil, datastore.NewDBError(err) + } + if err := json.Unmarshal(item.BinaryData["data"], ent); err != nil { + return nil, datastore.NewDBError(err) + } + list = append(list, ent) + } + return list, nil +} diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_suite_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_suite_test.go new file mode 100644 index 000000000..0f2956c96 --- /dev/null +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeapi + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestKubeapi(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Kubeapi Suite") +} diff --git a/pkg/apiserver/datastore/kubeapi/kubeapi_test.go b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go new file mode 100644 index 000000000..16f277f26 --- /dev/null +++ b/pkg/apiserver/datastore/kubeapi/kubeapi_test.go @@ -0,0 +1,167 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package kubeapi + +import ( + "context" + "fmt" + "math/rand" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var testScheme = runtime.NewScheme() + +var _ = BeforeSuite(func(done Done) { + rand.Seed(time.Now().UnixNano()) + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = scheme.AddToScheme(testScheme) + Expect(err).NotTo(HaveOccurred()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + close(done) +}, 240) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +}) + +var _ = Describe("Test kubeapi datastore driver", func() { + + clients.SetKubeClient(k8sClient) + kubeStore, err := New(context.TODO(), datastore.Config{Database: "test"}) + Expect(err).Should(BeNil()) + Expect(kubeStore).ToNot(BeNil()) + + It("Test add funtion", func() { + err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Test get funtion", func() { + app := &model.Application{Name: "kubevela-app"} + err := kubeStore.Get(context.TODO(), app) + Expect(err).Should(BeNil()) + diff := cmp.Diff(app.Description, "default") + Expect(diff).Should(BeEmpty()) + }) + + It("Test put funtion", func() { + err := kubeStore.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Test list funtion", func() { + err := kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}) + Expect(err).ShouldNot(HaveOccurred()) + err = kubeStore.Add(context.TODO(), &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"}) + Expect(err).ShouldNot(HaveOccurred()) + var app model.Application + list, err := kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) + Expect(err).ShouldNot(HaveOccurred()) + fmt.Printf("%+v", list[0]) + diff := cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) + + list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: 2, PageSize: 2}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 1) + Expect(diff).Should(BeEmpty()) + + list, err = kubeStore.List(context.TODO(), &app, &datastore.ListOptions{Page: 1, PageSize: 2}) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 2) + Expect(diff).Should(BeEmpty()) + + list, err = kubeStore.List(context.TODO(), &app, nil) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) + }) + + It("Test isExist funtion", func() { + var app model.Application + app.Name = "kubevela-app-3" + exist, err := kubeStore.IsExist(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + diff := cmp.Diff(exist, true) + Expect(diff).Should(BeEmpty()) + + app.Name = "kubevela-app-4" + notexist, err := kubeStore.IsExist(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + diff = cmp.Diff(notexist, false) + Expect(diff).Should(BeEmpty()) + }) + + It("Test delete funtion", func() { + var app model.Application + app.Name = "kubevela-app" + err := kubeStore.Delete(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + + app.Name = "kubevela-app-2" + err = kubeStore.Delete(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + + app.Name = "kubevela-app-3" + err = kubeStore.Delete(context.TODO(), &app) + Expect(err).ShouldNot(HaveOccurred()) + + app.Name = "kubevela-app-3" + err = kubeStore.Delete(context.TODO(), &app) + equal := cmp.Equal(err, datastore.ErrRecordNotExist, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + }) +}) diff --git a/pkg/apiserver/datastore/mongodb/mongodb.go b/pkg/apiserver/datastore/mongodb/mongodb.go index 3e34510a2..c2179e70e 100644 --- a/pkg/apiserver/datastore/mongodb/mongodb.go +++ b/pkg/apiserver/datastore/mongodb/mongodb.go @@ -27,6 +27,7 @@ import ( "go.mongodb.org/mongo-driver/mongo/options" "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" ) type mongodb struct { @@ -36,7 +37,7 @@ type mongodb struct { // New new mongodb datastore instance func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) { - if strings.HasPrefix(cfg.URL, "mongodb://") { + if !strings.HasPrefix(cfg.URL, "mongodb://") { cfg.URL = fmt.Sprintf("mongodb://%s", cfg.URL) } clientOpts := options.Client().ApplyURI(cfg.URL) @@ -53,70 +54,93 @@ func New(ctx context.Context, cfg datastore.Config) (datastore.DataStore, error) } // Add add data model -func (m *mongodb) Add(ctx context.Context, kind string, entity interface{}) error { - collection := m.client.Database(m.database).Collection(kind) +func (m *mongodb) Add(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + if err := m.Get(ctx, entity); err == nil { + return datastore.ErrRecordExist + } + collection := m.client.Database(m.database).Collection(entity.TableName()) _, err := collection.InsertOne(ctx, entity) if err != nil { - return err + return datastore.NewDBError(err) } return nil } // Get get data model -func (m *mongodb) Get(ctx context.Context, kind, name string, decodeTo interface{}) error { - collection := m.client.Database(m.database).Collection(kind) - return collection.FindOne(ctx, makeNameFilter(name)).Decode(decodeTo) -} - -// Put update data model -func (m *mongodb) Put(ctx context.Context, kind, name string, entity interface{}) error { - collection := m.client.Database(m.database).Collection(kind) - _, err := collection.UpdateOne(ctx, makeNameFilter(name), makeEntityUpdate(entity)) - if err != nil { - return err +func (m *mongodb) Get(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + collection := m.client.Database(m.database).Collection(entity.TableName()) + if err := collection.FindOne(ctx, makeNameFilter(entity.PrimaryKey())).Decode(entity); err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return datastore.ErrRecordNotExist + } + return datastore.NewDBError(err) } return nil } -// Find find data model -func (m *mongodb) Find(ctx context.Context, kind string) (datastore.Iterator, error) { - collection := m.client.Database(m.database).Collection(kind) - // bson.D{{}} specifies 'all documents' - filter := bson.D{} - cur, err := collection.Find(ctx, filter) - if err != nil { - return nil, err +// Put update data model +func (m *mongodb) Put(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty } - return &Iterator{cur: cur}, nil -} - -// FindOne find one data model -func (m *mongodb) FindOne(ctx context.Context, kind, name string) (datastore.Iterator, error) { - collection := m.client.Database(m.database).Collection(kind) - filter := bson.M{"name": name} - cur, err := collection.Find(ctx, filter) - if err != nil { - return nil, err + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty } - return &Iterator{cur: cur}, nil + collection := m.client.Database(m.database).Collection(entity.TableName()) + _, err := collection.UpdateOne(ctx, makeNameFilter(entity.PrimaryKey()), makeEntityUpdate(entity)) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return datastore.ErrRecordNotExist + } + return datastore.NewDBError(err) + } + return nil } // IsExist determine whether data exists. -func (m *mongodb) IsExist(ctx context.Context, kind, name string) (bool, error) { - collection := m.client.Database(m.database).Collection(kind) - err := collection.FindOne(ctx, makeNameFilter(name)).Err() +func (m *mongodb) IsExist(ctx context.Context, entity datastore.Entity) (bool, error) { + if entity.PrimaryKey() == "" { + return false, datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return false, datastore.ErrTableNameEmpty + } + collection := m.client.Database(m.database).Collection(entity.TableName()) + err := collection.FindOne(ctx, makeNameFilter(entity.PrimaryKey())).Err() if errors.Is(err, mongo.ErrNoDocuments) { return false, nil } else if err != nil { - return false, err + return false, datastore.NewDBError(err) } return true, nil } // Delete delete data -func (m *mongodb) Delete(ctx context.Context, kind, name string) error { - collection := m.client.Database(m.database).Collection(kind) +func (m *mongodb) Delete(ctx context.Context, entity datastore.Entity) error { + if entity.PrimaryKey() == "" { + return datastore.ErrPrimaryEmpty + } + if entity.TableName() == "" { + return datastore.ErrTableNameEmpty + } + // check entity is exist + if err := m.Get(ctx, entity); err != nil { + return err + } + collection := m.client.Database(m.database).Collection(entity.TableName()) // delete at most one document in which the "name" field is "Bob" or "bob" // specify the SetCollation option to provide a collation that will ignore case for string comparisons opts := options.Delete().SetCollation(&options.Collation{ @@ -124,8 +148,51 @@ func (m *mongodb) Delete(ctx context.Context, kind, name string) error { Strength: 1, CaseLevel: false, }) - _, err := collection.DeleteOne(ctx, makeNameFilter(name), opts) - return err + _, err := collection.DeleteOne(ctx, makeNameFilter(entity.PrimaryKey()), opts) + if err != nil { + log.Logger.Errorf("delete document failure %w", err) + return datastore.NewDBError(err) + } + return nil +} + +// List list entity function +func (m *mongodb) List(ctx context.Context, entity datastore.Entity, op *datastore.ListOptions) ([]datastore.Entity, error) { + if entity.TableName() == "" { + return nil, datastore.ErrTableNameEmpty + } + collection := m.client.Database(m.database).Collection(entity.TableName()) + // bson.D{{}} specifies 'all documents' + filter := bson.D{} + var findOptions options.FindOptions + if op != nil && op.PageSize > 0 && op.Page > 0 { + findOptions.SetSkip(int64(op.PageSize * (op.Page - 1))) + findOptions.SetLimit(int64(op.PageSize)) + } + cur, err := collection.Find(ctx, filter, &findOptions) + if err != nil { + return nil, datastore.NewDBError(err) + } + defer func() { + if err := cur.Close(ctx); err != nil { + log.Logger.Warnf("close mongodb cursor failure %s", err.Error()) + } + }() + var list []datastore.Entity + for cur.Next(ctx) { + item, err := datastore.NewEntity(entity) + if err != nil { + return nil, datastore.NewDBError(err) + } + if err := cur.Decode(item); err != nil { + return nil, datastore.NewDBError(fmt.Errorf("decode entity failure %w", err)) + } + list = append(list, item) + } + if err := cur.Err(); err != nil { + return nil, datastore.NewDBError(err) + } + return list, nil } func makeNameFilter(name string) bson.D { diff --git a/pkg/apiserver/datastore/mongodb/mongodb_suite_test.go b/pkg/apiserver/datastore/mongodb/mongodb_suite_test.go new file mode 100644 index 000000000..afa23b62e --- /dev/null +++ b/pkg/apiserver/datastore/mongodb/mongodb_suite_test.go @@ -0,0 +1,40 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mongodb + +import ( + "context" + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" +) + +func TestMongodb(t *testing.T) { + _, err := New(context.TODO(), datastore.Config{ + URL: "mongodb://localhost:27017", + Database: "kubevela", + }) + if err != nil { + t.Fatal(err) + } + + RegisterFailHandler(Fail) + RunSpecs(t, "Mongodb Suite") +} diff --git a/pkg/apiserver/datastore/mongodb/mongodb_test.go b/pkg/apiserver/datastore/mongodb/mongodb_test.go new file mode 100644 index 000000000..fe52b0b34 --- /dev/null +++ b/pkg/apiserver/datastore/mongodb/mongodb_test.go @@ -0,0 +1,136 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package mongodb + +import ( + "context" + "math/rand" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/model" +) + +var mongodbDriver datastore.DataStore +var _ = BeforeSuite(func(done Done) { + rand.Seed(time.Now().UnixNano()) + By("bootstrapping mongodb test environment") + var err error + mongodbDriver, err = New(context.TODO(), datastore.Config{ + URL: "mongodb://localhost:27017", + Database: "kubevela", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(mongodbDriver).ToNot(BeNil()) + + mongodbDriver, err = New(context.TODO(), datastore.Config{ + URL: "localhost:27017", + Database: "kubevela", + }) + Expect(err).ToNot(HaveOccurred()) + Expect(mongodbDriver).ToNot(BeNil()) + By("create mongodb driver success") + close(done) +}, 120) + +var _ = Describe("Test mongodb datastore driver", func() { + + It("Test add funtion", func() { + err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app", Description: "default"}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Test get funtion", func() { + app := &model.Application{Name: "kubevela-app"} + err := mongodbDriver.Get(context.TODO(), app) + Expect(err).ToNot(HaveOccurred()) + diff := cmp.Diff(app.Description, "default") + Expect(diff).Should(BeEmpty()) + }) + + It("Test put funtion", func() { + err := mongodbDriver.Put(context.TODO(), &model.Application{Name: "kubevela-app", Description: "this is demo"}) + Expect(err).ToNot(HaveOccurred()) + }) + + It("Test list funtion", func() { + err := mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app-2", Description: "this is demo 2"}) + Expect(err).ToNot(HaveOccurred()) + err = mongodbDriver.Add(context.TODO(), &model.Application{Name: "kubevela-app-3", Description: "this is demo 3"}) + Expect(err).ToNot(HaveOccurred()) + var app model.Application + list, err := mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: -1}) + Expect(err).ToNot(HaveOccurred()) + diff := cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) + + list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: 2, PageSize: 1}) + Expect(err).ToNot(HaveOccurred()) + diff = cmp.Diff(len(list), 1) + Expect(diff).Should(BeEmpty()) + + list, err = mongodbDriver.List(context.TODO(), &app, &datastore.ListOptions{Page: 1, PageSize: 2}) + Expect(err).ToNot(HaveOccurred()) + diff = cmp.Diff(len(list), 2) + Expect(diff).Should(BeEmpty()) + + list, err = mongodbDriver.List(context.TODO(), &app, nil) + Expect(err).ToNot(HaveOccurred()) + diff = cmp.Diff(len(list), 3) + Expect(diff).Should(BeEmpty()) + }) + + It("Test isExist funtion", func() { + var app model.Application + app.Name = "kubevela-app-3" + exist, err := mongodbDriver.IsExist(context.TODO(), &app) + Expect(err).ToNot(HaveOccurred()) + diff := cmp.Diff(exist, true) + Expect(diff).Should(BeEmpty()) + + app.Name = "kubevela-app-4" + notexist, err := mongodbDriver.IsExist(context.TODO(), &app) + Expect(err).ToNot(HaveOccurred()) + diff = cmp.Diff(notexist, false) + Expect(diff).Should(BeEmpty()) + }) + + It("Test delete funtion", func() { + var app model.Application + app.Name = "kubevela-app" + err := mongodbDriver.Delete(context.TODO(), &app) + Expect(err).ToNot(HaveOccurred()) + + app.Name = "kubevela-app-2" + err = mongodbDriver.Delete(context.TODO(), &app) + Expect(err).ToNot(HaveOccurred()) + + app.Name = "kubevela-app-3" + err = mongodbDriver.Delete(context.TODO(), &app) + Expect(err).ToNot(HaveOccurred()) + + app.Name = "kubevela-app-3" + err = mongodbDriver.Delete(context.TODO(), &app) + equal := cmp.Equal(err, datastore.ErrRecordNotExist, cmpopts.EquateErrors()) + Expect(equal).Should(BeTrue()) + }) +}) diff --git a/pkg/apiserver/model/application.go b/pkg/apiserver/model/application.go new file mode 100644 index 000000000..2c11bbd9a --- /dev/null +++ b/pkg/apiserver/model/application.go @@ -0,0 +1,36 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package model + +// Application database model +type Application struct { + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Labels map[string]string `json:"labels,omitempty"` + ClusterList []string `json:"clusterList,omitempty"` +} + +// TableName return custom table name +func (a *Application) TableName() string { + return tableNamePrefix + "application" +} + +// PrimaryKey return custom primary key +func (a *Application) PrimaryKey() string { + return a.Name +} diff --git a/pkg/apiserver/model/model.go b/pkg/apiserver/model/model.go new file mode 100644 index 000000000..7eb0cebd8 --- /dev/null +++ b/pkg/apiserver/model/model.go @@ -0,0 +1,19 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package model + +var tableNamePrefix = "vela_" diff --git a/pkg/apiserver/rest/apis/v1/types.go b/pkg/apiserver/rest/apis/v1/types.go index 4fb52ca36..4cc798fb6 100644 --- a/pkg/apiserver/rest/apis/v1/types.go +++ b/pkg/apiserver/rest/apis/v1/types.go @@ -18,6 +18,9 @@ package v1 import ( "time" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/common" + "github.com/oam-dev/kubevela/apis/types" ) // AddonPhase defines the phase of an addon @@ -36,7 +39,7 @@ const ( // CreateAddonRequest defines the format for addon create request type CreateAddonRequest struct { - Name string `json:"name" validate:"required"` + Name string `json:"name" validate:"name"` Version string `json:"version" validate:"required"` @@ -97,7 +100,7 @@ type AddonStatusResponse struct { // CreateClusterRequest request parameters to create a cluster type CreateClusterRequest struct { - Name string `json:"name" validate:"required"` + Name string `json:"name" validate:"name"` Description string `json:"description,omitempty"` Icon string `json:"icon"` KubeConfig string `json:"kubeConfig" validate:"required_without=kubeConfigSecret"` @@ -180,13 +183,15 @@ type GatewayRule struct { // CreateApplicationRequest create application request body type CreateApplicationRequest struct { - Name string `json:"name" validate:"required"` - Namespace string `json:"namespace" validate:"required"` + Name string `json:"name" validate:"checkname"` + Namespace string `json:"namespace" validate:"checkname"` Description string `json:"description"` Icon string `json:"icon"` Labels map[string]string `json:"labels,omitempty"` ClusterList []string `json:"clusterList,omitempty"` YamlConfig string `json:"yamlConfig,omitempty"` + // Deploy Setting this to true means that the application is deployed directly after creation. + Deploy bool `json:"deploy,omitempty"` } // DetailApplicationResponse application detail @@ -219,7 +224,7 @@ type ComponentBase struct { ComponentType string `json:"componentType"` BindClusters []string `json:"bindClusters"` Icon string `json:"icon,omitempty"` - DependOn []string `json:"dependOn"` + DependsOn []string `json:"dependsOn"` Creator string `json:"creator,omitempty"` DeployVersion string `json:"deployVersion"` } @@ -231,7 +236,7 @@ type ComponentListResponse struct { // CreateComponentRequest create component request model type CreateComponentRequest struct { - ApplicationName string `json:"appName" validate:"required"` + ApplicationName string `json:"appName" validate:"name"` Name string `json:"name" validate:"required"` Description string `json:"description"` Labels map[string]string `json:"labels,omitempty"` @@ -275,7 +280,7 @@ type NamesapceBase struct { // CreateNamespaceRequest create namespace request body type CreateNamespaceRequest struct { - Name string `json:"name" validate:"required"` + Name string `json:"name" validate:"name"` Description string `json:"description"` } @@ -292,17 +297,79 @@ type ListComponentDefinitionResponse struct { // ComponentDefinitionBase component definition base model type ComponentDefinitionBase struct { - Name string `json:"name"` - Description string `json:"description"` - Icon string `json:"icon"` - RequiredParams []Param `json:"requiredParams"` + Name string `json:"name"` + Description string `json:"description"` + Icon string `json:"icon"` + Parameter []types.Parameter `json:"requiredParams"` } -// Param For rendering forms -type Param struct { - Key string `json:"key"` - Name string `json:"name"` - DefaultValue interface{} `json:"defaultValue"` - Type string `json:"type"` - Description string `json:"description"` +// CreatePolicyRequest create app policy +type CreatePolicyRequest struct { + // Name is the unique name of the policy. + Name string `json:"name" validate:"name"` + + Type string `json:"type" validate:"required"` + + // Properties json data + Properties string `json:"properties"` +} + +// DetailPolicyResponse app policy detail model +type DetailPolicyResponse struct { + // Name is the unique name of the policy. + Name string `json:"name"` + + Type string `json:"type"` + + // Properties json data + Properties string `json:"properties"` +} + +// ListPolicyDefinitionResponse list available +type ListPolicyDefinitionResponse struct { + PolicyDefinitions []PolicyDefinition `json:"policyDefinitions"` +} + +// PolicyDefinition application policy definition +type PolicyDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters []types.Parameter `json:"parameters"` +} + +// UpdateWorkflowRequest update or create application workflow +type UpdateWorkflowRequest struct { + Steps []WorkflowStep `json:"steps,omitempty"` + Enable bool `json:"enable"` +} + +// WorkflowStep workflow step config +type WorkflowStep struct { + // Name is the unique name of the workflow step. + Name string `json:"name"` + + Type string `json:"type"` + + Properties string `json:"properties,omitempty"` + + Inputs common.StepInputs `json:"inputs,omitempty"` + + Outputs common.StepOutputs `json:"outputs,omitempty"` +} + +// DetailWorkflowResponse detail workflow response +type DetailWorkflowResponse struct { + Steps []WorkflowStep `json:"steps,omitempty"` + Enable bool `json:"enable"` + LastRecord *WorkflowRecord `json:"workflowRecord"` +} + +// ListWorkflowRecordsResponse list workflow execution record +type ListWorkflowRecordsResponse struct { + Records []WorkflowRecord `json:"records"` + Total int64 `json:"total"` +} + +// WorkflowRecord workflow record +type WorkflowRecord struct { } diff --git a/pkg/apiserver/rest/rest_server.go b/pkg/apiserver/rest/rest_server.go index 6d81b7862..8685f2166 100644 --- a/pkg/apiserver/rest/rest_server.go +++ b/pkg/apiserver/rest/rest_server.go @@ -82,7 +82,7 @@ func New(cfg Config) (a APIServer, err error) { } func (s *restServer) Run(ctx context.Context) error { - webservice.Init(ctx) + webservice.Init(ctx, s.dataStore) err := s.registerServices() if err != nil { return err diff --git a/pkg/apiserver/rest/usecase/application.go b/pkg/apiserver/rest/usecase/application.go new file mode 100644 index 000000000..783223c68 --- /dev/null +++ b/pkg/apiserver/rest/usecase/application.go @@ -0,0 +1,107 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package usecase + +import ( + "context" + "encoding/json" + "errors" + + "github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/log" + "github.com/oam-dev/kubevela/pkg/apiserver/model" + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" +) + +// ApplicationUsecase application usecase +type ApplicationUsecase interface { + CreateApplication(context.Context, apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) +} + +type applicationUsecaseImpl struct { + ds datastore.DataStore +} + +// NewApplicationUsecase new cluster usecase +func NewApplicationUsecase(ds datastore.DataStore) ApplicationUsecase { + return &applicationUsecaseImpl{ds: ds} +} + +// CreateApplication create application +func (c *applicationUsecaseImpl) CreateApplication(ctx context.Context, req apisv1.CreateApplicationRequest) (*apisv1.ApplicationBase, error) { + application := model.Application{ + Name: req.Name, + Description: req.Description, + Icon: req.Icon, + Labels: req.Labels, + ClusterList: req.ClusterList, + } + // check clusters. + + // check can deploy + var canDeploy bool + if req.YamlConfig != "" { + var oamApp v1beta1.Application + if err := json.Unmarshal([]byte(req.YamlConfig), &oamApp); err != nil { + log.Logger.Errorf("application yaml config is invalid,%s", err.Error()) + return nil, bcode.ErrApplicationConfig + } + // TODO: check oam spec + + // TODO: split the configuration and store it in the database. + + canDeploy = true + } + + // add to db. + if err := c.ds.Add(ctx, &application); err != nil { + if errors.Is(err, datastore.ErrRecordExist) { + return nil, bcode.ErrApplicationExist + } + return nil, err + } + // render app base info. + base := c.renderAppBase(&application) + // deploy to cluster if need. + if req.Deploy && canDeploy { + if err := c.Deploy(ctx, req.Name); err != nil { + return nil, err + } + } + return base, nil +} + +// Deploy deploy app to cluster +// means to render oam application config and apply to cluster. +// An event record is generated for each deploy. +func (c *applicationUsecaseImpl) Deploy(ctx context.Context, appName string) error { + // TODO: + return nil +} + +func (c *applicationUsecaseImpl) renderAppBase(app *model.Application) *apisv1.ApplicationBase { + appBeas := &apisv1.ApplicationBase{ + Name: app.Name, + Description: app.Description, + Icon: app.Icon, + Labels: app.Labels, + } + // TODO: get and render app status + return appBeas +} diff --git a/pkg/apiserver/rest/usecase/cluster_usecase.go b/pkg/apiserver/rest/usecase/cluster.go similarity index 100% rename from pkg/apiserver/rest/usecase/cluster_usecase.go rename to pkg/apiserver/rest/usecase/cluster.go diff --git a/pkg/apiserver/rest/utils/bcode/application.go b/pkg/apiserver/rest/utils/bcode/application.go new file mode 100644 index 000000000..273e12f3f --- /dev/null +++ b/pkg/apiserver/rest/utils/bcode/application.go @@ -0,0 +1,26 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package bcode + +// ErrApplicationConfig application config does not comply with OAM specification +var ErrApplicationConfig = NewBcode(400, 10000, "application config does not comply with OAM specification") + +// ErrComponentTypeNotSupport an unsupported component type was used. +var ErrComponentTypeNotSupport = NewBcode(400, 10001, "An unsupported component type was used.") + +// ErrApplicationExist application is exist +var ErrApplicationExist = NewBcode(400, 10002, "application name is exist") diff --git a/pkg/apiserver/rest/utils/bcode/bcode.go b/pkg/apiserver/rest/utils/bcode/bcode.go index 08495e248..3fab7ac7d 100644 --- a/pkg/apiserver/rest/utils/bcode/bcode.go +++ b/pkg/apiserver/rest/utils/bcode/bcode.go @@ -37,31 +37,48 @@ func (b *Bcode) Error() string { return fmt.Sprintf("HTTPCode:%d BusinessCode:%d Message:%s", b.HTTPCode, b.BusinessCode, b.Message) } +var bcodeMap map[int32]*Bcode + +// NewBcode new business code +func NewBcode(httpCode, businessCode int32, message string) *Bcode { + if bcodeMap == nil { + bcodeMap = make(map[int32]*Bcode) + } + if _, exit := bcodeMap[businessCode]; exit { + panic("bcode business code is exist") + } + bcode := &Bcode{HTTPCode: httpCode, BusinessCode: businessCode, Message: message} + bcodeMap[businessCode] = bcode + return bcode +} + // ReturnError Unified handling of all types of errors, generating a standard return structure. func ReturnError(req *restful.Request, res *restful.Response, err error) { var bcode *Bcode if errors.As(err, &bcode) { - if err := res.WriteEntity(err); err != nil { + if err := res.WriteHeaderAndEntity(int(bcode.HTTPCode), err); err != nil { log.Logger.Error("write entity failure %s", err.Error()) } return } - var restfulerr *restful.ServiceError - if errors.As(err, restfulerr) { - if err := res.WriteEntity(Bcode{HTTPCode: int32(restfulerr.Code), BusinessCode: int32(restfulerr.Code), Message: restfulerr.Message}); err != nil { + var restfulerr restful.ServiceError + if errors.As(err, &restfulerr) { + if err := res.WriteHeaderAndEntity(restfulerr.Code, Bcode{HTTPCode: int32(restfulerr.Code), BusinessCode: int32(restfulerr.Code), Message: restfulerr.Message}); err != nil { log.Logger.Error("write entity failure %s", err.Error()) } return } - var validErr *validator.ValidationErrors - if errors.As(err, validErr) { - if err := res.WriteEntity(Bcode{HTTPCode: 400, BusinessCode: 400, Message: err.Error()}); err != nil { + + var validErr validator.ValidationErrors + if errors.As(err, &validErr) { + if err := res.WriteHeaderAndEntity(400, Bcode{HTTPCode: 400, BusinessCode: 400, Message: err.Error()}); err != nil { log.Logger.Error("write entity failure %s", err.Error()) } return } - log.Logger.Errorf("Business exceptions, message %s, path:%s method:%s", err.Error(), req.Request.URL, req.Request.Method) - if err := res.WriteEntity(Bcode{HTTPCode: 500, BusinessCode: 500, Message: err.Error()}); err != nil { + + log.Logger.Errorf("Business exceptions, error message: %s, path:%s method:%s", err.Error(), req.Request.URL, req.Request.Method) + if err := res.WriteHeaderAndEntity(500, Bcode{HTTPCode: 500, BusinessCode: 500, Message: err.Error()}); err != nil { log.Logger.Error("write entity failure %s", err.Error()) } } diff --git a/pkg/apiserver/rest/webservice/application.go b/pkg/apiserver/rest/webservice/application.go index e768e9180..3ae3f2f22 100644 --- a/pkg/apiserver/rest/webservice/application.go +++ b/pkg/apiserver/rest/webservice/application.go @@ -21,9 +21,19 @@ import ( restful "github.com/emicklei/go-restful/v3" apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) type applicationWebService struct { + applicationUsecase usecase.ApplicationUsecase +} + +// NewApplicationWebService new application manage webservice +func NewApplicationWebService(applicationUsecase usecase.ApplicationUsecase) WebService { + return &applicationWebService{ + applicationUsecase: applicationUsecase, + } } func (c *applicationWebService) GetWebService() *restful.WebService { @@ -35,7 +45,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { tags := []string{"application"} - ws.Route(ws.GET("/").To(noop). + ws.Route(ws.GET("/").To(c.listApplications). Doc("list all applications"). Metadata(restfulspec.KeyOpenAPITags, tags). Param(ws.QueryParameter("query", "Fuzzy search based on name or description").DataType("string")). @@ -43,7 +53,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Param(ws.QueryParameter("cluster", "Cluster-based search").DataType("string")). Writes(apis.ListApplicationResponse{})) - ws.Route(ws.POST("/").To(noop). + ws.Route(ws.POST("/").To(c.createApplication). Doc("create one application"). Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateApplicationRequest{}). @@ -77,6 +87,7 @@ func (c *applicationWebService) GetWebService() *restful.WebService { ws.Route(ws.GET("/{name}/components").To(noop). Doc("gets the component topology of the application"). Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("cluster", "list components that deployed in define cluster").DataType("string")). Metadata(restfulspec.KeyOpenAPITags, tags). Writes(apis.ComponentListResponse{})) @@ -86,5 +97,55 @@ func (c *applicationWebService) GetWebService() *restful.WebService { Metadata(restfulspec.KeyOpenAPITags, tags). Reads(apis.CreateComponentRequest{}). Writes(apis.ComponentBase{})) + + ws.Route(ws.POST("/{name}/policies").To(noop). + Doc("create policy for application"). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Reads(apis.CreatePolicyRequest{}). + Writes(apis.DetailPolicyResponse{})) + + ws.Route(ws.GET("/{name}/policies/{policyName}").To(noop). + Doc("detail policy for application"). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.DetailPolicyResponse{})) + + ws.Route(ws.DELETE("/{name}/policies/{policyName}").To(noop). + Doc("detail policy for application"). + Param(ws.PathParameter("name", "identifier of the application").DataType("string")). + Param(ws.PathParameter("policyName", "identifier of the application policy").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.DetailPolicyResponse{})) return ws } + +func (c *applicationWebService) createApplication(req *restful.Request, res *restful.Response) { + // Verify the validity of parameters + var createReq apis.CreateApplicationRequest + if err := req.ReadEntity(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + if err := validate.Struct(&createReq); err != nil { + bcode.ReturnError(req, res, err) + return + } + // Call the usecase layer code + appBase, err := c.applicationUsecase.CreateApplication(req.Request.Context(), createReq) + if err != nil { + bcode.ReturnError(req, res, err) + return + } + + // Write back response data + if err := res.WriteEntity(appBase); err != nil { + bcode.ReturnError(req, res, err) + return + } +} + +func (c *applicationWebService) listApplications(req *restful.Request, res *restful.Response) { + +} diff --git a/pkg/apiserver/rest/webservice/cluster.go b/pkg/apiserver/rest/webservice/cluster.go index 9ff430740..9d6fed598 100644 --- a/pkg/apiserver/rest/webservice/cluster.go +++ b/pkg/apiserver/rest/webservice/cluster.go @@ -25,11 +25,18 @@ import ( "github.com/oam-dev/kubevela/pkg/apiserver/rest/utils/bcode" ) -type clusterWebService struct { +// ClusterWebService cluster manage webservice +type ClusterWebService struct { clusterUsecase usecase.ClusterUsecase } -func (c *clusterWebService) GetWebService() *restful.WebService { +// NewClusterWebService new cluster webservice +func NewClusterWebService(clusterUsecase usecase.ClusterUsecase) *ClusterWebService { + return &ClusterWebService{clusterUsecase: clusterUsecase} +} + +// GetWebService - +func (c *ClusterWebService) GetWebService() *restful.WebService { ws := new(restful.WebService) ws.Path(versionPrefix+"/clusters"). Consumes(restful.MIME_XML, restful.MIME_JSON). @@ -71,7 +78,7 @@ func (c *clusterWebService) GetWebService() *restful.WebService { return ws } -func (c *clusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) { +func (c *ClusterWebService) createKubeCluster(req *restful.Request, res *restful.Response) { // Verify the validity of parameters var createReq apis.CreateClusterRequest if err := req.ReadEntity(&createReq); err != nil { diff --git a/pkg/apiserver/rest/webservice/policy_definition.go b/pkg/apiserver/rest/webservice/policy_definition.go new file mode 100644 index 000000000..01f54ca17 --- /dev/null +++ b/pkg/apiserver/rest/webservice/policy_definition.go @@ -0,0 +1,43 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + restful "github.com/emicklei/go-restful/v3" + + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +type policyDefinitionWebservice struct { +} + +func (c *policyDefinitionWebservice) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/policydefinitions"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for policydefinition manage") + + tags := []string{"policydefinition"} + + ws.Route(ws.GET("/").To(noop). + Doc("list all policydefinition"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.ListPolicyDefinitionResponse{})) + return ws +} diff --git a/pkg/apiserver/rest/webservice/validate.go b/pkg/apiserver/rest/webservice/validate.go new file mode 100644 index 000000000..ff4f063ea --- /dev/null +++ b/pkg/apiserver/rest/webservice/validate.go @@ -0,0 +1,42 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice + +import ( + "regexp" + + "github.com/go-playground/validator/v10" +) + +var validate = validator.New() + +var nameRegexp = regexp.MustCompile(`^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$`) + +func init() { + if err := validate.RegisterValidation("checkname", ValidateName); err != nil { + panic(err) + } +} + +// ValidateName custom check name field +func ValidateName(fl validator.FieldLevel) bool { + value := fl.Field().String() + if len(value) > 32 || len(value) < 2 { + return false + } + return nameRegexp.MatchString(value) +} diff --git a/pkg/apiserver/rest/webservice/validate_test.go b/pkg/apiserver/rest/webservice/validate_test.go new file mode 100644 index 000000000..f6fd675b7 --- /dev/null +++ b/pkg/apiserver/rest/webservice/validate_test.go @@ -0,0 +1,56 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice + +import ( + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test validate function", func() { + It("Test check name validate ", func() { + Expect(cmp.Diff(nameRegexp.MatchString("///Asd asda "), false)).Should(BeEmpty()) + var app0 = apisv1.CreateApplicationRequest{ + Name: "a", + Namespace: "namesapce", + } + err := validate.Struct(&app0) + Expect(err).ShouldNot(BeNil()) + var app1 = apisv1.CreateApplicationRequest{ + Name: "Asdasd", + Namespace: "namesapce", + } + err = validate.Struct(&app1) + Expect(err).ShouldNot(BeNil()) + var app2 = apisv1.CreateApplicationRequest{ + Name: "asdasd asdasd ++", + Namespace: "namesapce", + } + err = validate.Struct(&app2) + Expect(err).ShouldNot(BeNil()) + + var app3 = apisv1.CreateApplicationRequest{ + Name: "asdasd", + Namespace: "namesapce", + } + err = validate.Struct(&app3) + Expect(err).Should(BeNil()) + }) +}) diff --git a/pkg/apiserver/rest/webservice/webservice.go b/pkg/apiserver/rest/webservice/webservice.go index 690bb3fe8..86b11edaa 100644 --- a/pkg/apiserver/rest/webservice/webservice.go +++ b/pkg/apiserver/rest/webservice/webservice.go @@ -21,14 +21,14 @@ import ( "net/http" "github.com/emicklei/go-restful/v3" - "github.com/go-playground/validator/v10" + + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + "github.com/oam-dev/kubevela/pkg/apiserver/rest/usecase" ) // versionPrefix API version prefix. var versionPrefix = "/api/v1" -var validate = validator.New() - // WebService webservice interface type WebService interface { GetWebService() *restful.WebService @@ -57,11 +57,16 @@ func returns500(b *restful.RouteBuilder) { } // Init init all webservice, pass in the required parameter object. -func Init(ctx context.Context) { - RegistWebService(&clusterWebService{}) - RegistWebService(&applicationWebService{}) +// It can be implemented using the idea of dependency injection. +func Init(ctx context.Context, ds datastore.DataStore) { + clusterUsecase := usecase.NewClusterUsecase(ds) + applicationUsecase := usecase.NewApplicationUsecase(ds) + RegistWebService(NewClusterWebService(clusterUsecase)) + RegistWebService(NewApplicationWebService(applicationUsecase)) RegistWebService(&namespaceWebService{}) RegistWebService(&componentDefinitionWebservice{}) RegistWebService(&addonWebService{}) RegistWebService(&oamApplicationWebService{}) + RegistWebService(&policyDefinitionWebservice{}) + RegistWebService(&workflowWebService{}) } diff --git a/pkg/apiserver/rest/webservice/webservice_suite_test.go b/pkg/apiserver/rest/webservice/webservice_suite_test.go new file mode 100644 index 000000000..342e017b5 --- /dev/null +++ b/pkg/apiserver/rest/webservice/webservice_suite_test.go @@ -0,0 +1,29 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +func TestWebservice(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Webservice Suite") +} diff --git a/pkg/apiserver/rest/webservice/workflow.go b/pkg/apiserver/rest/webservice/workflow.go new file mode 100644 index 000000000..9fef7662b --- /dev/null +++ b/pkg/apiserver/rest/webservice/workflow.go @@ -0,0 +1,60 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webservice + +import ( + restfulspec "github.com/emicklei/go-restful-openapi/v2" + restful "github.com/emicklei/go-restful/v3" + + apis "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +type workflowWebService struct { +} + +func (c *workflowWebService) GetWebService() *restful.WebService { + ws := new(restful.WebService) + ws.Path(versionPrefix+"/workflows"). + Consumes(restful.MIME_XML, restful.MIME_JSON). + Produces(restful.MIME_JSON, restful.MIME_XML). + Doc("api for cluster manage") + + tags := []string{"cluster"} + + ws.Route(ws.GET("/{name}").To(noop). + Doc("detail application workflow"). + Param(ws.PathParameter("name", "identifier of the workflow, Currently, the application name is used.").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.PUT("/{name}").To(noop). + Doc("create or update application workflow config"). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Reads(apis.UpdateWorkflowRequest{}). + Writes(apis.DetailWorkflowResponse{}).Do(returns200, returns500)) + + ws.Route(ws.GET("/{name}/records").To(noop). + Doc("query application workflow execution record"). + Param(ws.PathParameter("name", "identifier of the workflow").DataType("string")). + Metadata(restfulspec.KeyOpenAPITags, tags). + Param(ws.PathParameter("page", "Query the page number.").DataType("integer")). + Param(ws.PathParameter("pageSize", "Query the page size number.").DataType("integer")). + Writes(apis.ListWorkflowRecordsResponse{}).Do(returns200, returns500)) + + return ws +} diff --git a/test/e2e-apiserver-test/application_test.go b/test/e2e-apiserver-test/application_test.go new file mode 100644 index 000000000..adb82a2d1 --- /dev/null +++ b/test/e2e-apiserver-test/application_test.go @@ -0,0 +1,57 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e_apiserver_test + +import ( + "bytes" + "encoding/json" + "net/http" + + "github.com/google/go-cmp/cmp" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + apisv1 "github.com/oam-dev/kubevela/pkg/apiserver/rest/apis/v1" +) + +var _ = Describe("Test application rest api", func() { + It("Test create app", func() { + defer GinkgoRecover() + var req = apisv1.CreateApplicationRequest{ + Name: "test-app-sadasd", + Namespace: "test-app-namesapce", + Description: "this is a test app", + Icon: "", + Labels: map[string]string{"test": "true"}, + ClusterList: []string{}, + } + bodyByte, err := json.Marshal(req) + Expect(err).ShouldNot(HaveOccurred()) + res, err := http.Post("http://127.0.0.1:8000/api/v1/applications", "application/json", bytes.NewBuffer(bodyByte)) + Expect(err).ShouldNot(HaveOccurred()) + Expect(res).ShouldNot(BeNil()) + Expect(cmp.Diff(res.StatusCode, 200)).Should(BeEmpty()) + Expect(res.Body).ShouldNot(BeNil()) + defer res.Body.Close() + var appBase apisv1.ApplicationBase + err = json.NewDecoder(res.Body).Decode(&appBase) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cmp.Diff(appBase.Name, req.Name)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Description, req.Description)).Should(BeEmpty()) + Expect(cmp.Diff(appBase.Labels["test"], req.Labels["test"])).Should(BeEmpty()) + }) +}) diff --git a/test/e2e-apiserver-test/suite_test.go b/test/e2e-apiserver-test/suite_test.go new file mode 100644 index 000000000..dfe95b2bb --- /dev/null +++ b/test/e2e-apiserver-test/suite_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2021 The KubeVela Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package e2e_apiserver_test + +import ( + "context" + "testing" + "time" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + + "github.com/oam-dev/kubevela/pkg/apiserver/clients" + "github.com/oam-dev/kubevela/pkg/apiserver/datastore" + arest "github.com/oam-dev/kubevela/pkg/apiserver/rest" +) + +var cfg *rest.Config +var k8sClient client.Client +var testEnv *envtest.Environment +var testScheme = runtime.NewScheme() + +func TestE2eApiserverTest(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "E2eApiserverTest Suite") +} + +var _ = BeforeSuite(func() { + + By("bootstrapping test environment") + + testEnv = &envtest.Environment{ + ControlPlaneStartTimeout: time.Minute * 3, + ControlPlaneStopTimeout: time.Minute, + UseExistingCluster: pointer.BoolPtr(false), + } + + By("start kube test env") + var err error + cfg, err = testEnv.Start() + Expect(err).ShouldNot(HaveOccurred()) + Expect(cfg).ToNot(BeNil()) + + err = scheme.AddToScheme(testScheme) + Expect(err).NotTo(HaveOccurred()) + + By("new kube client") + cfg.Timeout = time.Minute * 2 + k8sClient, err = client.New(cfg, client.Options{Scheme: testScheme}) + Expect(err).Should(BeNil()) + Expect(k8sClient).ToNot(BeNil()) + By("new kube client success") + clients.SetKubeClient(k8sClient) + + ctx := context.Background() + + server, err := arest.New(arest.Config{ + BindAddr: "127.0.0.1:8000", + Datastore: datastore.Config{ + Type: "kubeapi", + Database: "kubevela", + }, + }) + Expect(err).ShouldNot(HaveOccurred()) + Expect(server).ShouldNot(BeNil()) + go func() { + err = server.Run(ctx) + Expect(err).ShouldNot(HaveOccurred()) + }() + By("api server started") + time.Sleep(time.Second * 2) +}) + +var _ = AfterSuite(func() { + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).ToNot(HaveOccurred()) +})